chisel/
args.rs

1use crate::{
2    history::chisel_history_file,
3    prelude::{ChiselCommand, ChiselDispatcher, DispatchResult, SolidityHelper},
4};
5use clap::Parser;
6use eyre::{Context, Result};
7use foundry_cli::{
8    handler,
9    utils::{self, LoadConfig},
10};
11use foundry_common::fs;
12use foundry_config::{
13    figment::{
14        value::{Dict, Map},
15        Metadata, Profile, Provider,
16    },
17    Config,
18};
19use rustyline::{config::Configurer, error::ReadlineError, Editor};
20use std::path::PathBuf;
21use tracing::debug;
22use yansi::Paint;
23
24use crate::opts::{Chisel, ChiselSubcommand};
25
26/// Run the `chisel` command line interface.
27pub fn run() -> Result<()> {
28    setup()?;
29
30    let args = Chisel::parse();
31    args.global.init()?;
32
33    run_command(args)
34}
35
36/// Setup the global logger and other utilities.
37pub fn setup() -> Result<()> {
38    handler::install();
39    utils::subscriber();
40    utils::load_dotenv();
41
42    Ok(())
43}
44
45/// Run the subcommand.
46#[tokio::main]
47pub async fn run_command(args: Chisel) -> Result<()> {
48    // Keeps track of whether or not an interrupt was the last input
49    let mut interrupt = false;
50
51    // Load configuration
52    let (config, evm_opts) = args.load_config_and_evm_opts()?;
53
54    // Create a new cli dispatcher
55    let mut dispatcher = ChiselDispatcher::new(crate::session_source::SessionSourceConfig {
56        // Enable traces if any level of verbosity was passed
57        traces: config.verbosity > 0,
58        foundry_config: config,
59        no_vm: args.no_vm,
60        evm_opts,
61        backend: None,
62        calldata: None,
63    })?;
64
65    // Execute prelude Solidity source files
66    evaluate_prelude(&mut dispatcher, args.prelude).await?;
67
68    // Check for chisel subcommands
69    match &args.cmd {
70        Some(ChiselSubcommand::List) => {
71            let sessions = dispatcher.dispatch_command(ChiselCommand::ListSessions, &[]).await;
72            match sessions {
73                DispatchResult::CommandSuccess(Some(session_list)) => {
74                    sh_println!("{session_list}")?;
75                }
76                DispatchResult::CommandFailed(e) => sh_err!("{e}")?,
77                _ => panic!("Unexpected result: Please report this bug."),
78            }
79            return Ok(())
80        }
81        Some(ChiselSubcommand::Load { id }) | Some(ChiselSubcommand::View { id }) => {
82            // For both of these subcommands, we need to attempt to load the session from cache
83            match dispatcher.dispatch_command(ChiselCommand::Load, &[id]).await {
84                DispatchResult::CommandSuccess(_) => { /* Continue */ }
85                DispatchResult::CommandFailed(e) => {
86                    sh_err!("{e}")?;
87                    return Ok(())
88                }
89                _ => panic!("Unexpected result! Please report this bug."),
90            }
91
92            // If the subcommand was `view`, print the source and exit.
93            if matches!(args.cmd, Some(ChiselSubcommand::View { .. })) {
94                match dispatcher.dispatch_command(ChiselCommand::Source, &[]).await {
95                    DispatchResult::CommandSuccess(Some(source)) => {
96                        sh_println!("{source}")?;
97                    }
98                    _ => panic!("Unexpected result! Please report this bug."),
99                }
100                return Ok(())
101            }
102        }
103        Some(ChiselSubcommand::ClearCache) => {
104            match dispatcher.dispatch_command(ChiselCommand::ClearCache, &[]).await {
105                DispatchResult::CommandSuccess(Some(msg)) => sh_println!("{}", msg.green())?,
106                DispatchResult::CommandFailed(e) => sh_err!("{e}")?,
107                _ => panic!("Unexpected result! Please report this bug."),
108            }
109            return Ok(())
110        }
111        Some(ChiselSubcommand::Eval { command }) => {
112            dispatch_repl_line(&mut dispatcher, command).await?;
113            return Ok(())
114        }
115        None => { /* No chisel subcommand present; Continue */ }
116    }
117
118    // Create a new rustyline Editor
119    let mut rl = Editor::<SolidityHelper, _>::new()?;
120    rl.set_helper(Some(SolidityHelper::default()));
121
122    // automatically add lines to history
123    rl.set_auto_add_history(true);
124
125    // load history
126    if let Some(chisel_history) = chisel_history_file() {
127        let _ = rl.load_history(&chisel_history);
128    }
129
130    // Print welcome header
131    sh_println!("Welcome to Chisel! Type `{}` to show available commands.", "!help".green())?;
132
133    // Begin Rustyline loop
134    loop {
135        // Get the prompt from the dispatcher
136        // Variable based on status of the last entry
137        let prompt = dispatcher.get_prompt();
138
139        // Read the next line
140        let next_string = rl.readline(prompt.as_ref());
141
142        // Try to read the string
143        match next_string {
144            Ok(line) => {
145                debug!("dispatching next line: {line}");
146                // Clear interrupt flag
147                interrupt = false;
148
149                // Dispatch and match results
150                let errored = dispatch_repl_line(&mut dispatcher, &line).await?;
151                rl.helper_mut().unwrap().set_errored(errored);
152            }
153            Err(ReadlineError::Interrupted) => {
154                if interrupt {
155                    break
156                } else {
157                    sh_println!("(To exit, press Ctrl+C again)")?;
158                    interrupt = true;
159                }
160            }
161            Err(ReadlineError::Eof) => break,
162            Err(err) => {
163                sh_err!("{err:?}")?;
164                break
165            }
166        }
167    }
168
169    if let Some(chisel_history) = chisel_history_file() {
170        let _ = rl.save_history(&chisel_history);
171    }
172
173    Ok(())
174}
175
176/// [Provider] impl
177impl Provider for Chisel {
178    fn metadata(&self) -> Metadata {
179        Metadata::named("Script Args Provider")
180    }
181
182    fn data(&self) -> Result<Map<Profile, Dict>, foundry_config::figment::Error> {
183        Ok(Map::from([(Config::selected_profile(), Dict::default())]))
184    }
185}
186
187/// Evaluate a single Solidity line.
188async fn dispatch_repl_line(dispatcher: &mut ChiselDispatcher, line: &str) -> Result<bool> {
189    let r = dispatcher.dispatch(line).await;
190    match &r {
191        DispatchResult::Success(msg) | DispatchResult::CommandSuccess(msg) => {
192            debug!(%line, ?msg, "dispatch success");
193            if let Some(msg) = msg {
194                sh_println!("{}", msg.green())?;
195            }
196        },
197        DispatchResult::UnrecognizedCommand(e) => sh_err!("{e}")?,
198        DispatchResult::SolangParserFailed(e) => {
199            sh_err!("{}", "Compilation error".red())?;
200            sh_eprintln!("{}", format!("{e:?}").red())?;
201        }
202        DispatchResult::FileIoError(e) => sh_err!("{}", format!("File IO - {e}").red())?,
203        DispatchResult::CommandFailed(msg) | DispatchResult::Failure(Some(msg)) => sh_err!("{}", msg.red())?,
204        DispatchResult::Failure(None) => sh_err!("Please report this bug as a github issue if it persists: https://github.com/foundry-rs/foundry/issues/new/choose")?,
205    }
206    Ok(r.is_error())
207}
208
209/// Evaluate multiple Solidity source files contained within a
210/// Chisel prelude directory.
211async fn evaluate_prelude(
212    dispatcher: &mut ChiselDispatcher,
213    maybe_prelude: Option<PathBuf>,
214) -> Result<()> {
215    let Some(prelude_dir) = maybe_prelude else { return Ok(()) };
216    if prelude_dir.is_file() {
217        sh_println!("{} {}", "Loading prelude source file:".yellow(), prelude_dir.display())?;
218        load_prelude_file(dispatcher, prelude_dir).await?;
219        sh_println!("{}\n", "Prelude source file loaded successfully!".green())?;
220    } else {
221        let prelude_sources = fs::files_with_ext(&prelude_dir, "sol");
222        let mut print_success_msg = false;
223        for source_file in prelude_sources {
224            print_success_msg = true;
225            sh_println!("{} {}", "Loading prelude source file:".yellow(), source_file.display())?;
226            load_prelude_file(dispatcher, source_file).await?;
227        }
228
229        if print_success_msg {
230            sh_println!("{}\n", "All prelude source files loaded successfully!".green())?;
231        }
232    }
233    Ok(())
234}
235
236/// Loads a single Solidity file into the prelude.
237async fn load_prelude_file(dispatcher: &mut ChiselDispatcher, file: PathBuf) -> Result<()> {
238    let prelude = fs::read_to_string(file)
239        .wrap_err("Could not load source file. Are you sure this path is correct?")?;
240    dispatch_repl_line(dispatcher, &prelude).await?;
241    Ok(())
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use clap::CommandFactory;
248
249    #[test]
250    fn verify_cli() {
251        Chisel::command().debug_assert();
252    }
253}