From e2a6282a52ebe62775ae4dda76d97898da4a1228 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:11:05 +0100 Subject: [PATCH] feat: add global `--json` flag (#9244) * add global json --flag * finish port to shell::is_json * fix test * update message * very strange stalling bug, fixed by assignment? * remove jobs -j shorthand clashing with global json flag * fix test after -j change * fix doctests * temporarily disable junit conflict, revert -j as --json shorthand * tag --color, --quiet as conflicting with --json * update tests to be aware of global args to avoid `Argument or group quiet specified in conflicts_with* for junit does not exist` error * fix missed test * make sure tests throw on non-matching command * use --format-json in command to show alias works --- crates/cast/bin/args.rs | 28 ------------ crates/cast/bin/cmd/access_list.rs | 8 +--- crates/cast/bin/cmd/call.rs | 7 +-- crates/cast/bin/cmd/interface.rs | 10 ++--- crates/cast/bin/cmd/logs.rs | 20 ++------- crates/cast/bin/cmd/send.rs | 15 ++----- crates/cast/bin/cmd/wallet/mod.rs | 26 +++++------- crates/cast/bin/main.rs | 30 ++++++------- crates/cast/src/lib.rs | 54 +++++++++--------------- crates/cast/tests/cli/main.rs | 3 ++ crates/cli/src/opts/shell.rs | 20 ++++++++- crates/common/src/io/shell.rs | 43 ++++++++++++++++++- crates/forge/bin/cmd/build.rs | 15 +++---- crates/forge/bin/cmd/create.rs | 9 ++-- crates/forge/bin/cmd/test/mod.rs | 68 +++++++++++++++++++----------- crates/forge/bin/main.rs | 3 +- crates/forge/tests/cli/build.rs | 4 +- crates/forge/tests/cli/cmd.rs | 3 ++ crates/script/src/lib.rs | 8 +--- crates/verify/src/bytecode.rs | 19 +++------ crates/verify/src/utils.rs | 7 ++- 21 files changed, 192 insertions(+), 208 deletions(-) diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index 03ca46d23826..a16436dd2ea7 100644 --- a/crates/cast/bin/args.rs +++ b/crates/cast/bin/args.rs @@ -369,10 +369,6 @@ pub enum CastSubcommand { #[arg(long, env = "CAST_FULL_BLOCK")] full: bool, - /// Print the block as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, - #[command(flatten)] rpc: RpcOpts, }, @@ -464,10 +460,6 @@ pub enum CastSubcommand { #[arg(long, conflicts_with = "field")] raw: bool, - /// Print as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, - #[command(flatten)] rpc: RpcOpts, }, @@ -489,10 +481,6 @@ pub enum CastSubcommand { #[arg(id = "async", long = "async", env = "CAST_ASYNC", alias = "cast-async")] cast_async: bool, - /// Print as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, - #[command(flatten)] rpc: RpcOpts, }, @@ -530,10 +518,6 @@ pub enum CastSubcommand { /// The ABI-encoded calldata. calldata: String, - - /// Print the decoded calldata as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, }, /// Decode ABI-encoded string. @@ -543,10 +527,6 @@ pub enum CastSubcommand { StringDecode { /// The ABI-encoded string. data: String, - - /// Print the decoded string as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, }, /// Decode ABI-encoded input or output data. @@ -565,10 +545,6 @@ pub enum CastSubcommand { /// Whether to decode the input or output data. #[arg(long, short, help_heading = "Decode input data instead of output data")] input: bool, - - /// Print the decoded calldata as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, }, /// ABI encode the given function argument, excluding the selector. @@ -655,10 +631,6 @@ pub enum CastSubcommand { FourByteDecode { /// The ABI-encoded calldata. calldata: Option, - - /// Print the decoded calldata as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, }, /// Get the event signature for a given topic 0 from https://openchain.xyz. diff --git a/crates/cast/bin/cmd/access_list.rs b/crates/cast/bin/cmd/access_list.rs index 3d60b52903a3..c283f60c40b0 100644 --- a/crates/cast/bin/cmd/access_list.rs +++ b/crates/cast/bin/cmd/access_list.rs @@ -35,10 +35,6 @@ pub struct AccessListArgs { #[arg(long, short = 'B')] block: Option, - /// Print the access list as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, - #[command(flatten)] tx: TransactionOpts, @@ -48,7 +44,7 @@ pub struct AccessListArgs { impl AccessListArgs { pub async fn run(self) -> Result<()> { - let Self { to, sig, args, tx, eth, block, json: to_json } = self; + let Self { to, sig, args, tx, eth, block } = self; let config = Config::from(ð); let provider = utils::get_provider(&config)?; @@ -65,7 +61,7 @@ impl AccessListArgs { let cast = Cast::new(&provider); - let access_list: String = cast.access_list(&tx, block, to_json).await?; + let access_list: String = cast.access_list(&tx, block).await?; sh_println!("{access_list}")?; diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index 355d18ee6ad7..aefc5f1c02f5 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -69,10 +69,6 @@ pub struct CallArgs { #[arg(long, short)] block: Option, - /// Print the decoded output as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, - /// Enable Alphanet features. #[arg(long, alias = "odyssey")] pub alphanet: bool, @@ -131,7 +127,6 @@ impl CallArgs { decode_internal, labels, data, - json, .. } = self; @@ -205,7 +200,7 @@ impl CallArgs { return Ok(()); } - sh_println!("{}", Cast::new(provider).call(&tx, func.as_ref(), block, json).await?)?; + sh_println!("{}", Cast::new(provider).call(&tx, func.as_ref(), block).await?)?; Ok(()) } diff --git a/crates/cast/bin/cmd/interface.rs b/crates/cast/bin/cmd/interface.rs index 45df4983e4f0..659ce6ccaa4f 100644 --- a/crates/cast/bin/cmd/interface.rs +++ b/crates/cast/bin/cmd/interface.rs @@ -4,7 +4,7 @@ use clap::Parser; use eyre::{Context, Result}; use foundry_block_explorers::Client; use foundry_cli::opts::EtherscanOpts; -use foundry_common::{compile::ProjectCompiler, fs}; +use foundry_common::{compile::ProjectCompiler, fs, shell}; use foundry_compilers::{info::ContractInfo, utils::canonicalize}; use foundry_config::{load_config_with_root, try_find_project_root, Config}; use itertools::Itertools; @@ -44,17 +44,13 @@ pub struct InterfaceArgs { )] output: Option, - /// If specified, the interface will be output as JSON rather than Solidity. - #[arg(long, short)] - json: bool, - #[command(flatten)] etherscan: EtherscanOpts, } impl InterfaceArgs { pub async fn run(self) -> Result<()> { - let Self { contract, name, pragma, output: output_location, etherscan, json } = self; + let Self { contract, name, pragma, output: output_location, etherscan } = self; // Determine if the target contract is an ABI file, a local contract or an Ethereum address. let abis = if Path::new(&contract).is_file() && @@ -75,7 +71,7 @@ impl InterfaceArgs { let interfaces = get_interfaces(abis)?; // Print result or write to file. - let res = if json { + let res = if shell::is_json() { // Format as JSON. interfaces.iter().map(|iface| &iface.json_abi).format("\n").to_string() } else { diff --git a/crates/cast/bin/cmd/logs.rs b/crates/cast/bin/cmd/logs.rs index b38281d51e34..154b5c9d2135 100644 --- a/crates/cast/bin/cmd/logs.rs +++ b/crates/cast/bin/cmd/logs.rs @@ -49,26 +49,14 @@ pub struct LogsArgs { #[arg(long)] subscribe: bool, - /// Print the logs as JSON.s - #[arg(long, short, help_heading = "Display options")] - json: bool, - #[command(flatten)] eth: EthereumOpts, } impl LogsArgs { pub async fn run(self) -> Result<()> { - let Self { - from_block, - to_block, - address, - sig_or_topic, - topics_or_args, - subscribe, - json, - eth, - } = self; + let Self { from_block, to_block, address, sig_or_topic, topics_or_args, subscribe, eth } = + self; let config = Config::from(ð); let provider = utils::get_provider(&config)?; @@ -88,7 +76,7 @@ impl LogsArgs { let filter = build_filter(from_block, to_block, address, sig_or_topic, topics_or_args)?; if !subscribe { - let logs = cast.filter_logs(filter, json).await?; + let logs = cast.filter_logs(filter).await?; sh_println!("{logs}")?; return Ok(()) } @@ -102,7 +90,7 @@ impl LogsArgs { .await?; let cast = Cast::new(&provider); let mut stdout = io::stdout(); - cast.subscribe(filter, &mut stdout, json).await?; + cast.subscribe(filter, &mut stdout).await?; Ok(()) } diff --git a/crates/cast/bin/cmd/send.rs b/crates/cast/bin/cmd/send.rs index 9503bccbd549..77b6a2cddae5 100644 --- a/crates/cast/bin/cmd/send.rs +++ b/crates/cast/bin/cmd/send.rs @@ -39,10 +39,6 @@ pub struct SendTxArgs { #[arg(long, default_value = "1")] confirmations: u64, - /// Print the transaction receipt as JSON. - #[arg(long, short, help_heading = "Display options")] - json: bool, - #[command(subcommand)] command: Option, @@ -98,7 +94,6 @@ impl SendTxArgs { mut args, tx, confirmations, - json: to_json, command, unlocked, path, @@ -159,7 +154,7 @@ impl SendTxArgs { let (tx, _) = builder.build(config.sender).await?; - cast_send(provider, tx, cast_async, confirmations, timeout, to_json).await + cast_send(provider, tx, cast_async, confirmations, timeout).await // Case 2: // An option to use a local signer was provided. // If we cannot successfully instantiate a local signer, then we will assume we don't have @@ -178,7 +173,7 @@ impl SendTxArgs { .wallet(wallet) .on_provider(&provider); - cast_send(provider, tx, cast_async, confirmations, timeout, to_json).await + cast_send(provider, tx, cast_async, confirmations, timeout).await } } } @@ -189,7 +184,6 @@ async fn cast_send, T: Transport + Clone>( cast_async: bool, confs: u64, timeout: u64, - to_json: bool, ) -> Result<()> { let cast = Cast::new(provider); let pending_tx = cast.send(tx).await?; @@ -199,9 +193,8 @@ async fn cast_send, T: Transport + Clone>( if cast_async { sh_println!("{tx_hash:#x}")?; } else { - let receipt = cast - .receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false, to_json) - .await?; + let receipt = + cast.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?; sh_println!("{receipt}")?; } diff --git a/crates/cast/bin/cmd/wallet/mod.rs b/crates/cast/bin/cmd/wallet/mod.rs index 1dc2fb1c5824..8023b8bdff33 100644 --- a/crates/cast/bin/cmd/wallet/mod.rs +++ b/crates/cast/bin/cmd/wallet/mod.rs @@ -11,7 +11,7 @@ use cast::revm::primitives::Authorization; use clap::Parser; use eyre::{Context, Result}; use foundry_cli::{opts::RpcOpts, utils}; -use foundry_common::{fs, sh_println}; +use foundry_common::{fs, sh_println, shell}; use foundry_config::Config; use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner}; use rand::thread_rng; @@ -49,10 +49,6 @@ pub enum WalletSubcommands { /// Number of wallets to generate. #[arg(long, short, default_value = "1")] number: u32, - - /// Output generated wallets as JSON. - #[arg(long, short, default_value = "false")] - json: bool, }, /// Generates a random BIP39 mnemonic phrase @@ -69,10 +65,6 @@ pub enum WalletSubcommands { /// Entropy to use for the mnemonic #[arg(long, short, conflicts_with = "words")] entropy: Option, - - /// Output generated mnemonic phrase and accounts as JSON. - #[arg(long, short, default_value = "false")] - json: bool, }, /// Generate a vanity address. @@ -219,10 +211,10 @@ pub enum WalletSubcommands { impl WalletSubcommands { pub async fn run(self) -> Result<()> { match self { - Self::New { path, unsafe_password, number, json, .. } => { + Self::New { path, unsafe_password, number, .. } => { let mut rng = thread_rng(); - let mut json_values = if json { Some(vec![]) } else { None }; + let mut json_values = if shell::is_json() { Some(vec![]) } else { None }; if let Some(path) = path { let path = match dunce::canonicalize(path.clone()) { Ok(path) => path, @@ -294,7 +286,7 @@ impl WalletSubcommands { } } } - Self::NewMnemonic { words, accounts, entropy, json } => { + Self::NewMnemonic { words, accounts, entropy } => { let phrase = if let Some(entropy) = entropy { let entropy = Entropy::from_slice(hex::decode(entropy)?)?; Mnemonic::::new_from_entropy(entropy).to_phrase() @@ -303,7 +295,9 @@ impl WalletSubcommands { Mnemonic::::new_with_count(&mut rng, words)?.to_phrase() }; - if !json { + let format_json = shell::is_json(); + + if !format_json { sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?; } @@ -315,7 +309,7 @@ impl WalletSubcommands { let wallets = wallets.into_iter().map(|b| b.build()).collect::, _>>()?; - if !json { + if !format_json { sh_println!("{}", "Successfully generated a new mnemonic.".green())?; sh_println!("Phrase:\n{phrase}")?; sh_println!("\nAccounts:")?; @@ -324,7 +318,7 @@ impl WalletSubcommands { let mut accounts = json!([]); for (i, wallet) in wallets.iter().enumerate() { let private_key = hex::encode(wallet.credential().to_bytes()); - if json { + if format_json { accounts.as_array_mut().unwrap().push(json!({ "address": format!("{}", wallet.address()), "private_key": format!("0x{}", private_key), @@ -336,7 +330,7 @@ impl WalletSubcommands { } } - if json { + if format_json { let obj = json!({ "mnemonic": phrase, "accounts": accounts, diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index f7d66ef9d0d0..851fcfd77490 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -19,7 +19,7 @@ use foundry_common::{ import_selectors, parse_signatures, pretty_calldata, ParsedSignatures, SelectorImportData, SelectorType, }, - stdin, + shell, stdin, }; use foundry_config::Config; use std::time::Instant; @@ -187,9 +187,9 @@ async fn main_args(args: CastArgs) -> Result<()> { } // ABI encoding & decoding - CastSubcommand::AbiDecode { sig, calldata, input, json } => { + CastSubcommand::AbiDecode { sig, calldata, input } => { let tokens = SimpleCast::abi_decode(&sig, &calldata, input)?; - print_tokens(&tokens, json) + print_tokens(&tokens, shell::is_json()) } CastSubcommand::AbiEncode { sig, packed, args } => { if !packed { @@ -198,16 +198,16 @@ async fn main_args(args: CastArgs) -> Result<()> { sh_println!("{}", SimpleCast::abi_encode_packed(&sig, &args)?)? } } - CastSubcommand::CalldataDecode { sig, calldata, json } => { + CastSubcommand::CalldataDecode { sig, calldata } => { let tokens = SimpleCast::calldata_decode(&sig, &calldata, true)?; - print_tokens(&tokens, json) + print_tokens(&tokens, shell::is_json()) } CastSubcommand::CalldataEncode { sig, args } => { sh_println!("{}", SimpleCast::calldata_encode(sig, &args)?)?; } - CastSubcommand::StringDecode { data, json } => { + CastSubcommand::StringDecode { data } => { let tokens = SimpleCast::calldata_decode("Any(string)", &data, true)?; - print_tokens(&tokens, json) + print_tokens(&tokens, shell::is_json()) } CastSubcommand::Interface(cmd) => cmd.run().await?, CastSubcommand::CreationCode(cmd) => cmd.run().await?, @@ -271,13 +271,13 @@ async fn main_args(args: CastArgs) -> Result<()> { Cast::new(provider).base_fee(block.unwrap_or(BlockId::Number(Latest))).await? )? } - CastSubcommand::Block { block, full, field, json, rpc } => { + CastSubcommand::Block { block, full, field, rpc } => { let config = Config::from(&rpc); let provider = utils::get_provider(&config)?; sh_println!( "{}", Cast::new(provider) - .block(block.unwrap_or(BlockId::Number(Latest)), full, field, json) + .block(block.unwrap_or(BlockId::Number(Latest)), full, field) .await? )? } @@ -432,26 +432,26 @@ async fn main_args(args: CastArgs) -> Result<()> { sh_println!("{}", serde_json::json!(receipt))?; } } - CastSubcommand::Receipt { tx_hash, field, json, cast_async, confirmations, rpc } => { + CastSubcommand::Receipt { tx_hash, field, cast_async, confirmations, rpc } => { let config = Config::from(&rpc); let provider = utils::get_provider(&config)?; sh_println!( "{}", Cast::new(provider) - .receipt(tx_hash, field, confirmations, None, cast_async, json) + .receipt(tx_hash, field, confirmations, None, cast_async) .await? )? } CastSubcommand::Run(cmd) => cmd.run().await?, CastSubcommand::SendTx(cmd) => cmd.run().await?, - CastSubcommand::Tx { tx_hash, field, raw, json, rpc } => { + CastSubcommand::Tx { tx_hash, field, raw, rpc } => { let config = Config::from(&rpc); let provider = utils::get_provider(&config)?; // Can use either --raw or specify raw as a field let raw = raw || field.as_ref().is_some_and(|f| f == "raw"); - sh_println!("{}", Cast::new(&provider).transaction(tx_hash, field, raw, json).await?)? + sh_println!("{}", Cast::new(&provider).transaction(tx_hash, field, raw).await?)? } // 4Byte @@ -465,7 +465,7 @@ async fn main_args(args: CastArgs) -> Result<()> { sh_println!("{sig}")? } } - CastSubcommand::FourByteDecode { calldata, json } => { + CastSubcommand::FourByteDecode { calldata } => { let calldata = stdin::unwrap_line(calldata)?; let sigs = decode_calldata(&calldata).await?; sigs.iter().enumerate().for_each(|(i, sig)| { @@ -482,7 +482,7 @@ async fn main_args(args: CastArgs) -> Result<()> { }; let tokens = SimpleCast::calldata_decode(sig, &calldata, true)?; - print_tokens(&tokens, json) + print_tokens(&tokens, shell::is_json()) } CastSubcommand::FourByteEvent { topic } => { let topic = stdin::unwrap_line(topic)?; diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index bfa33c6e93f8..75272b3b1a99 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -27,7 +27,7 @@ use foundry_common::{ abi::{encode_function_args, get_func}, compile::etherscan_project, fmt::*, - fs, get_pretty_tx_receipt_attr, TransactionReceiptWithRevertReason, + fs, get_pretty_tx_receipt_attr, shell, TransactionReceiptWithRevertReason, }; use foundry_compilers::flatten::Flattener; use foundry_config::Chain; @@ -123,7 +123,7 @@ where /// let tx = TransactionRequest::default().to(to).input(bytes.into()); /// let tx = WithOtherFields::new(tx); /// let cast = Cast::new(alloy_provider); - /// let data = cast.call(&tx, None, None, false).await?; + /// let data = cast.call(&tx, None, None).await?; /// println!("{}", data); /// # Ok(()) /// # } @@ -133,7 +133,6 @@ where req: &WithOtherFields, func: Option<&Function>, block: Option, - json: bool, ) -> Result { let res = self.provider.call(req).block(block.unwrap_or_default()).await?; @@ -174,7 +173,7 @@ where // handle case when return type is not specified Ok(if decoded.is_empty() { res.to_string() - } else if json { + } else if shell::is_json() { let tokens = decoded.iter().map(format_token_raw).collect::>(); serde_json::to_string_pretty(&tokens).unwrap() } else { @@ -208,7 +207,7 @@ where /// let tx = TransactionRequest::default().to(to).input(bytes.into()); /// let tx = WithOtherFields::new(tx); /// let cast = Cast::new(&provider); - /// let access_list = cast.access_list(&tx, None, false).await?; + /// let access_list = cast.access_list(&tx, None).await?; /// println!("{}", access_list); /// # Ok(()) /// # } @@ -217,11 +216,10 @@ where &self, req: &WithOtherFields, block: Option, - to_json: bool, ) -> Result { let access_list = self.provider.create_access_list(req).block_id(block.unwrap_or_default()).await?; - let res = if to_json { + let res = if shell::is_json() { serde_json::to_string(&access_list)? } else { let mut s = @@ -329,7 +327,7 @@ where /// let provider = /// ProviderBuilder::<_, _, AnyNetwork>::default().on_builtin("http://localhost:8545").await?; /// let cast = Cast::new(provider); - /// let block = cast.block(5, true, None, false).await?; + /// let block = cast.block(5, true, None).await?; /// println!("{}", block); /// # Ok(()) /// # } @@ -339,7 +337,6 @@ where block: B, full: bool, field: Option, - to_json: bool, ) -> Result { let block = block.into(); if let Some(ref field) = field { @@ -357,7 +354,7 @@ where let block = if let Some(ref field) = field { get_pretty_block_attr(&block, field) .unwrap_or_else(|| format!("{field} is not a valid block field")) - } else if to_json { + } else if shell::is_json() { serde_json::to_value(&block).unwrap().to_string() } else { block.pretty() @@ -374,7 +371,6 @@ where false, // Select only select field Some(field), - false, ) .await?; @@ -408,14 +404,12 @@ where false, // Select only block hash Some(String::from("hash")), - false, ) .await?; Ok(match &genesis_hash[..] { "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" => { - match &(Self::block(self, 1920000, false, Some("hash".to_string()), false).await?)[..] - { + match &(Self::block(self, 1920000, false, Some("hash".to_string())).await?)[..] { "0x94365e3a8c0b35089c1d1195081fe7489b528a84b22199c916180db8b28ade7f" => { "etclive" } @@ -453,7 +447,7 @@ where "0x6d3c66c5357ec91d5c43af47e234a939b22557cbb552dc45bebbceeed90fbe34" => "bsctest", "0x0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b" => "bsc", "0x31ced5b9beb7f8782b014660da0cb18cc409f121f408186886e1ca3e8eeca96b" => { - match &(Self::block(self, 1, false, Some(String::from("hash")), false).await?)[..] { + match &(Self::block(self, 1, false, Some(String::from("hash"))).await?)[..] { "0x738639479dc82d199365626f90caa82f7eafcfe9ed354b456fb3d294597ceb53" => { "avalanche-fuji" } @@ -718,7 +712,7 @@ where /// ProviderBuilder::<_, _, AnyNetwork>::default().on_builtin("http://localhost:8545").await?; /// let cast = Cast::new(provider); /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc"; - /// let tx = cast.transaction(tx_hash.to_string(), None, false, false).await?; + /// let tx = cast.transaction(tx_hash.to_string(), None, false).await?; /// println!("{}", tx); /// # Ok(()) /// # } @@ -728,7 +722,6 @@ where tx_hash: String, field: Option, raw: bool, - to_json: bool, ) -> Result { let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?; let tx = self @@ -742,7 +735,7 @@ where } else if let Some(field) = field { get_pretty_tx_attr(&tx, field.as_str()) .ok_or_else(|| eyre::eyre!("invalid tx field: {}", field.to_string()))? - } else if to_json { + } else if shell::is_json() { // to_value first to sort json object keys serde_json::to_value(&tx)?.to_string() } else { @@ -761,7 +754,7 @@ where /// ProviderBuilder::<_, _, AnyNetwork>::default().on_builtin("http://localhost:8545").await?; /// let cast = Cast::new(provider); /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc"; - /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false, false).await?; + /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?; /// println!("{}", receipt); /// # Ok(()) /// # } @@ -773,7 +766,6 @@ where confs: u64, timeout: Option, cast_async: bool, - to_json: bool, ) -> Result { let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?; @@ -802,7 +794,7 @@ where Ok(if let Some(ref field) = field { get_pretty_tx_receipt_attr(&receipt, field) .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))? - } else if to_json { + } else if shell::is_json() { // to_value first to sort json object keys serde_json::to_value(&receipt)?.to_string() } else { @@ -878,10 +870,10 @@ where )) } - pub async fn filter_logs(&self, filter: Filter, to_json: bool) -> Result { + pub async fn filter_logs(&self, filter: Filter) -> Result { let logs = self.provider.get_logs(&filter).await?; - let res = if to_json { + let res = if shell::is_json() { serde_json::to_string(&logs)? } else { let mut s = vec![]; @@ -969,16 +961,11 @@ where /// let filter = /// Filter::new().address(Address::from_str("0x00000000006c3852cbEf3e08E8dF289169EdE581")?); /// let mut output = io::stdout(); - /// cast.subscribe(filter, &mut output, false).await?; + /// cast.subscribe(filter, &mut output).await?; /// # Ok(()) /// # } /// ``` - pub async fn subscribe( - &self, - filter: Filter, - output: &mut dyn io::Write, - to_json: bool, - ) -> Result<()> { + pub async fn subscribe(&self, filter: Filter, output: &mut dyn io::Write) -> Result<()> { // Initialize the subscription stream for logs let mut subscription = self.provider.subscribe_logs(&filter).await?.into_stream(); @@ -989,10 +976,11 @@ where None }; + let format_json = shell::is_json(); let to_block_number = filter.get_to_block(); // If output should be JSON, start with an opening bracket - if to_json { + if format_json { write!(output, "[")?; } @@ -1014,7 +1002,7 @@ where }, // Process incoming log log = subscription.next() => { - if to_json { + if format_json { if !first { write!(output, ",")?; } @@ -1037,7 +1025,7 @@ where } // If output was JSON, end with a closing bracket - if to_json { + if format_json { write!(output, "]")?; } diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 8a8d512f83c2..6fb3ddb73c58 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -40,6 +40,9 @@ Display options: - always: Force color output - never: Force disable color output + --json + Format log messages as JSON + -q, --quiet Do not print log messages diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs index 9213c670224c..fd83b952bef0 100644 --- a/crates/cli/src/opts/shell.rs +++ b/crates/cli/src/opts/shell.rs @@ -1,5 +1,5 @@ use clap::Parser; -use foundry_common::shell::{ColorChoice, Shell, Verbosity}; +use foundry_common::shell::{ColorChoice, OutputFormat, Shell, Verbosity}; // note: `verbose` and `quiet` cannot have `short` because of conflicts with multiple commands. @@ -21,6 +21,16 @@ pub struct ShellOpts { )] pub quiet: bool, + /// Format log messages as JSON. + #[clap( + long, + global = true, + alias = "format-json", + conflicts_with_all = &["quiet", "color"], + help_heading = "Display options" + )] + pub json: bool, + /// Log messages coloring. #[clap(long, global = true, value_enum, help_heading = "Display options")] pub color: Option, @@ -34,6 +44,12 @@ impl ShellOpts { (false, false) => Verbosity::Normal, (true, true) => unreachable!(), }; - Shell::new_with(self.color.unwrap_or_default(), verbosity) + let color = self.json.then_some(ColorChoice::Never).or(self.color).unwrap_or_default(); + let format = match self.json { + true => OutputFormat::Json, + false => OutputFormat::Text, + }; + + Shell::new_with(format, color, verbosity) } } diff --git a/crates/common/src/io/shell.rs b/crates/common/src/io/shell.rs index ee217fa728e8..45d9c2296037 100644 --- a/crates/common/src/io/shell.rs +++ b/crates/common/src/io/shell.rs @@ -27,6 +27,11 @@ pub fn is_quiet() -> bool { verbosity().is_quiet() } +/// Returns whether the output format is [`OutputFormat::Json`]. +pub fn is_json() -> bool { + Shell::get().output_format().is_json() +} + /// The global shell instance. static GLOBAL_SHELL: OnceLock> = OnceLock::new(); @@ -95,6 +100,30 @@ impl Verbosity { } } +/// The requested output format. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum OutputFormat { + /// Plain text output. + #[default] + Text, + /// JSON output. + Json, +} + +impl OutputFormat { + /// Returns true if the output format is `Text`. + #[inline] + pub fn is_text(self) -> bool { + self == Self::Text + } + + /// Returns true if the output format is `Json`. + #[inline] + pub fn is_json(self) -> bool { + self == Self::Json + } +} + /// An abstraction around console output that remembers preferences for output /// verbosity and color. pub struct Shell { @@ -102,6 +131,9 @@ pub struct Shell { /// output to a memory buffer which is useful for tests. output: ShellOut, + /// The format to use for message output. + output_format: OutputFormat, + /// How verbose messages should be. verbosity: Verbosity, @@ -158,12 +190,12 @@ impl Shell { /// output. #[inline] pub fn new() -> Self { - Self::new_with(ColorChoice::Auto, Verbosity::Verbose) + Self::new_with(OutputFormat::Text, 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 { + pub fn new_with(format: OutputFormat, color: ColorChoice, verbosity: Verbosity) -> Self { Self { output: ShellOut::Stream { stdout: AutoStream::new(std::io::stdout(), color.to_anstream_color_choice()), @@ -171,6 +203,7 @@ impl Shell { color_choice: color, stderr_tty: std::io::stderr().is_terminal(), }, + output_format: format, verbosity, needs_clear: AtomicBool::new(false), } @@ -181,6 +214,7 @@ impl Shell { pub fn empty() -> Self { Self { output: ShellOut::Empty(std::io::empty()), + output_format: OutputFormat::Text, verbosity: Verbosity::Quiet, needs_clear: AtomicBool::new(false), } @@ -238,6 +272,11 @@ impl Shell { self.verbosity } + /// Gets the output format of the shell. + pub fn output_format(&self) -> OutputFormat { + self.output_format + } + /// Gets the current color choice. /// /// If we are not using a color stream, this will always return `Never`, even if the color diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index f6d348334232..f2a1891e2634 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -2,7 +2,7 @@ use super::{install, watch::WatchArgs}; use clap::Parser; use eyre::Result; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; -use foundry_common::compile::ProjectCompiler; +use foundry_common::{compile::ProjectCompiler, shell}; use foundry_compilers::{ compilers::{multi::MultiCompilerLanguage, Language}, utils::source_files_iter, @@ -73,12 +73,6 @@ pub struct BuildArgs { #[command(flatten)] #[serde(skip)] pub watch: WatchArgs, - - /// 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 = "quiet")] - #[serde(skip)] - pub format_json: bool, } impl BuildArgs { @@ -102,17 +96,18 @@ impl BuildArgs { } } + let format_json = shell::is_json(); let compiler = ProjectCompiler::new() .files(files) .print_names(self.names) .print_sizes(self.sizes) .ignore_eip_3860(self.ignore_eip_3860) - .quiet(self.format_json) - .bail(!self.format_json); + .quiet(format_json) + .bail(!format_json); let output = compiler.compile(&project)?; - if self.format_json { + if format_json { sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?; } diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index e9f6cac745e1..93de30e98eab 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -18,6 +18,7 @@ use foundry_cli::{ use foundry_common::{ compile::{self}, fmt::parse_tokens, + shell, }; use foundry_compilers::{artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize}; use foundry_config::{ @@ -57,10 +58,6 @@ pub struct CreateArgs { )] constructor_args_path: Option, - /// Print the deployment information as JSON. - #[arg(long, help_heading = "Display options")] - json: bool, - /// Verify contract after creation. #[arg(long)] verify: bool, @@ -109,7 +106,7 @@ impl CreateArgs { project.find_contract_path(&self.contract.name)? }; - let mut output = compile::compile_target(&target_path, &project, self.json)?; + let mut output = compile::compile_target(&target_path, &project, shell::is_json())?; let (abi, bin, _) = remove_contract(&mut output, &target_path, &self.contract.name)?; @@ -315,7 +312,7 @@ impl CreateArgs { let (deployed_contract, receipt) = deployer.send_with_receipt().await?; let address = deployed_contract; - if self.json { + if shell::is_json() { let output = json!({ "deployer": deployer_address.to_string(), "deployedTo": address.to_string(), diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 4c973217f4d7..e2707b82fc19 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::{compile::ProjectCompiler, evm::EvmArgs, fs}; +use foundry_common::{compile::ProjectCompiler, evm::EvmArgs, fs, shell}; use foundry_compilers::{ artifacts::output_selection::OutputSelection, compilers::{multi::MultiCompilerLanguage, CompilerSettings, Language}, @@ -118,12 +118,8 @@ pub struct TestArgs { #[arg(long, env = "FORGE_ALLOW_FAILURE")] allow_failure: bool, - /// Output test results in JSON format. - #[arg(long, help_heading = "Display options")] - pub json: bool, - /// Output test results as JUnit XML report. - #[arg(long, conflicts_with_all(["json", "gas_report"]), help_heading = "Display options")] + #[arg(long, conflicts_with_all = ["quiet", "json", "gas_report"], help_heading = "Display options")] pub junit: bool, /// Stop running tests after the first failure. @@ -308,7 +304,7 @@ impl TestArgs { let sources_to_compile = self.get_sources_to_compile(&config, &filter)?; let compiler = - ProjectCompiler::new().quiet(self.json || self.junit).files(sources_to_compile); + ProjectCompiler::new().quiet(shell::is_json() || self.junit).files(sources_to_compile); let output = compiler.compile(&project)?; @@ -480,13 +476,13 @@ impl TestArgs { output: &ProjectCompileOutput, ) -> eyre::Result { if self.list { - return list(runner, filter, self.json); + return list(runner, filter); } trace!(target: "forge::test", "running all tests"); // If we need to render to a serialized format, we should not print anything else to stdout. - let silent = self.gas_report && self.json; + let silent = self.gas_report && shell::is_json(); let num_filtered = runner.matching_test_functions(filter).count(); if num_filtered != 1 && (self.debug.is_some() || self.flamegraph || self.flamechart) { @@ -514,7 +510,7 @@ impl TestArgs { } // Run tests in a non-streaming fashion and collect results for serialization. - if !self.gas_report && self.json { + if !self.gas_report && shell::is_json() { let mut results = runner.test_collect(filter); results.values_mut().for_each(|suite_result| { for test_result in suite_result.test_results.values_mut() { @@ -584,7 +580,7 @@ impl TestArgs { config.gas_reports.clone(), config.gas_reports_ignore.clone(), config.gas_reports_include_tests, - if self.json { GasReportKind::JSON } else { GasReportKind::Markdown }, + if shell::is_json() { GasReportKind::JSON } else { GasReportKind::Markdown }, ) }); @@ -908,14 +904,10 @@ impl Provider for TestArgs { } /// Lists all matching tests -fn list( - runner: MultiContractRunner, - filter: &ProjectPathsAwareFilter, - json: bool, -) -> Result { +fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result { let results = runner.list(filter); - if json { + if shell::is_json() { println!("{}", serde_json::to_string(&results)?); } else { for (file, contracts) in results.iter() { @@ -999,34 +991,56 @@ fn junit_xml_report(results: &BTreeMap, verbosity: u8) -> R #[cfg(test)] mod tests { + use crate::opts::{Forge, ForgeSubcommand}; + use super::*; use foundry_config::{Chain, InvariantConfig}; use foundry_test_utils::forgetest_async; #[test] fn watch_parse() { - let args: TestArgs = TestArgs::parse_from(["foundry-cli", "-vw"]); + let args = match Forge::parse_from(["foundry-cli", "test", "-vw"]).cmd { + ForgeSubcommand::Test(args) => args, + _ => unreachable!(), + }; assert!(args.watch.watch.is_some()); } #[test] fn fuzz_seed() { - let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--fuzz-seed", "0x10"]); + let args = match Forge::parse_from(["foundry-cli", "test", "--fuzz-seed", "0x10"]).cmd { + ForgeSubcommand::Test(args) => args, + _ => unreachable!(), + }; assert!(args.fuzz_seed.is_some()); } // #[test] fn fuzz_seed_exists() { - let args: TestArgs = - TestArgs::parse_from(["foundry-cli", "-vvv", "--gas-report", "--fuzz-seed", "0x10"]); + let args = match Forge::parse_from([ + "foundry-cli", + "test", + "-vvv", + "--gas-report", + "--fuzz-seed", + "0x10", + ]) + .cmd + { + ForgeSubcommand::Test(args) => args, + _ => unreachable!(), + }; assert!(args.fuzz_seed.is_some()); } #[test] fn extract_chain() { let test = |arg: &str, expected: Chain| { - let args = TestArgs::parse_from(["foundry-cli", arg]); + let args = match Forge::parse_from(["foundry-cli", "test", arg]).cmd { + ForgeSubcommand::Test(args) => args, + _ => unreachable!(), + }; assert_eq!(args.evm_opts.env.chain, Some(expected)); let (config, evm_opts) = args.load_config_and_evm_opts().unwrap(); assert_eq!(config.chain, Some(expected)); @@ -1080,12 +1094,18 @@ contract FooBarTest is DSTest { ) .unwrap(); - let args = TestArgs::parse_from([ + let args = match Forge::parse_from([ "foundry-cli", + "test", "--gas-report", "--root", &prj.root().to_string_lossy(), - ]); + ]) + .cmd + { + ForgeSubcommand::Test(args) => args, + _ => unreachable!(), + }; let outcome = args.run().await.unwrap(); let gas_report = outcome.gas_report.unwrap(); diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index c713a703ee7b..ee49f7f692b2 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -2,6 +2,7 @@ use clap::{CommandFactory, Parser}; use clap_complete::generate; use eyre::Result; use foundry_cli::{handler, utils}; +use foundry_common::shell; use foundry_evm::inspectors::cheatcodes::{set_execution_context, ForgeContext}; mod cmd; @@ -42,7 +43,7 @@ fn run() -> Result<()> { if cmd.is_watch() { utils::block_on(watch::watch_test(cmd)) } else { - let silent = cmd.junit || cmd.json; + let silent = cmd.junit || shell::is_json(); let outcome = utils::block_on(cmd.run())?; outcome.ensure_ok(silent) } diff --git a/crates/forge/tests/cli/build.rs b/crates/forge/tests/cli/build.rs index 812754c72552..4b257fe8d20a 100644 --- a/crates/forge/tests/cli/build.rs +++ b/crates/forge/tests/cli/build.rs @@ -22,9 +22,9 @@ 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' +error: the argument '--json' cannot be used with '--quiet' -Usage: forge[..] build --format-json [PATHS]... +Usage: forge[..] build --json [PATHS]... For more information, try '--help'. diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index f9e8997bd99c..1560850b26f5 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -45,6 +45,9 @@ Display options: - always: Force color output - never: Force disable color output + --json + Format log messages as JSON + -q, --quiet Do not print log messages diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index ec19c0bf9716..707399d6305d 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -30,7 +30,7 @@ use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_common::{ abi::{encode_function_args, get_func}, evm::{Breakpoints, EvmArgs}, - ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN, + shell, ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; use foundry_compilers::ArtifactId; use foundry_config::{ @@ -180,10 +180,6 @@ pub struct ScriptArgs { #[arg(long)] pub verify: bool, - /// Output results in JSON format. - #[arg(long)] - pub json: bool, - /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either /// specified in wei, or as a string with a unit type. /// @@ -262,7 +258,7 @@ impl ScriptArgs { }; } - if pre_simulation.args.json { + if shell::is_json() { pre_simulation.show_json()?; } else { pre_simulation.show_traces().await?; diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index 02ca28c20066..a4f368a961f7 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -16,6 +16,7 @@ use foundry_cli::{ opts::EtherscanOpts, utils::{self, read_constructor_args_file, LoadConfig}, }; +use foundry_common::shell; use foundry_compilers::{artifacts::EvmVersion, info::ContractInfo}; use foundry_config::{figment, impl_figment_convert, Config}; use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, utils::configure_tx_env}; @@ -75,10 +76,6 @@ pub struct VerifyBytecodeArgs { #[clap(flatten)] pub verifier: VerifierArgs, - /// Suppress logs and emit json results to stdout - #[clap(long, default_value = "false")] - pub json: bool, - /// The project's root path. /// /// By default root of the Git repository, if in one, @@ -144,7 +141,7 @@ impl VerifyBytecodeArgs { eyre::bail!("No bytecode found at address {}", self.address); } - if !self.json { + if !shell::is_json() { println!( "Verifying bytecode for contract {} at address {}", self.contract.name.clone().green(), @@ -214,7 +211,7 @@ impl VerifyBytecodeArgs { crate::utils::check_args_len(&artifact, &constructor_args)?; if maybe_predeploy { - if !self.json { + if !shell::is_json() { println!( "{}", format!("Attempting to verify predeployed contract at {:?}. Ignoring creation code verification.", self.address) @@ -290,7 +287,6 @@ impl VerifyBytecodeArgs { ); crate::utils::print_result( - &self, match_type, BytecodeType::Runtime, &mut json_results, @@ -298,7 +294,7 @@ impl VerifyBytecodeArgs { &config, ); - if self.json { + if shell::is_json() { sh_println!("{}", serde_json::to_string(&json_results)?)?; } @@ -376,7 +372,6 @@ impl VerifyBytecodeArgs { ); crate::utils::print_result( - &self, match_type, BytecodeType::Creation, &mut json_results, @@ -387,14 +382,13 @@ impl VerifyBytecodeArgs { // If the creation code does not match, the runtime also won't match. Hence return. if match_type.is_none() { crate::utils::print_result( - &self, None, BytecodeType::Runtime, &mut json_results, etherscan_metadata, &config, ); - if self.json { + if shell::is_json() { sh_println!("{}", serde_json::to_string(&json_results)?)?; } return Ok(()); @@ -488,7 +482,6 @@ impl VerifyBytecodeArgs { ); crate::utils::print_result( - &self, match_type, BytecodeType::Runtime, &mut json_results, @@ -497,7 +490,7 @@ impl VerifyBytecodeArgs { ); } - if self.json { + if shell::is_json() { sh_println!("{}", serde_json::to_string(&json_results)?)?; } Ok(()) diff --git a/crates/verify/src/utils.rs b/crates/verify/src/utils.rs index aaf8a0e016b7..e3065aca0853 100644 --- a/crates/verify/src/utils.rs +++ b/crates/verify/src/utils.rs @@ -9,7 +9,7 @@ use foundry_block_explorers::{ contract::{ContractCreationData, ContractMetadata, Metadata}, errors::EtherscanError, }; -use foundry_common::{abi::encode_args, compile::ProjectCompiler, provider::RetryProvider}; +use foundry_common::{abi::encode_args, compile::ProjectCompiler, provider::RetryProvider, shell}; use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion}; use foundry_config::Config; use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, opts::EvmOpts}; @@ -137,7 +137,6 @@ pub fn build_using_cache( } pub fn print_result( - args: &VerifyBytecodeArgs, res: Option, bytecode_type: BytecodeType, json_results: &mut Vec, @@ -145,7 +144,7 @@ pub fn print_result( config: &Config, ) { if let Some(res) = res { - if !args.json { + if !shell::is_json() { println!( "{} with status {}", format!("{bytecode_type:?} code matched").green().bold(), @@ -155,7 +154,7 @@ pub fn print_result( let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None }; json_results.push(json_res); } - } else if !args.json { + } else if !shell::is_json() { println!( "{}", format!(