Skip to content

feat(shell): initial draft #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
773 changes: 750 additions & 23 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"cast",
"forge",
"cli",
"shell",
]

# Binary size optimizations
Expand Down
20 changes: 20 additions & 0 deletions shell/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "forge-shell"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Foundry's Solidity shell"

[dependencies]
colored = "2.0.0"
ctrlc = "3.2.1"
rustyline = "9.1.1"
shellwords = "1.1.0"
structopt = "0.3.25"
ethers = { git = "https://github.com/gakonst/ethers-rs" }
regex = { version = "1.5.4", default-features = false }
eyre = "0.6.5"
dirs-next = "2.0.0"
log = "0.4.14"
solang = { git = "https://github.com/hyperledger-labs/solang", default-features = false }
pretty_env_logger = "0.4.0"
26 changes: 26 additions & 0 deletions shell/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::path::PathBuf;
use structopt::{clap::AppSettings, StructOpt};

/// Main forge-shell args
#[derive(Debug, StructOpt)]
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
pub struct Args {
#[structopt(
help = "Select a series of libraries to pre load into session. These can be directories or single files."
)]
pub libs: Vec<PathBuf>,
#[structopt(
help = "Select a whole project to load into session during start up.",
short = "w",
long = "workspace"
)]
pub workspace: Option<PathBuf>,

#[structopt(subcommand)]
pub subcommand: Option<SubCommand>,
}

#[derive(Debug, StructOpt)]
pub enum SubCommand {
// TODO want subcommands do we need?
}
12 changes: 12 additions & 0 deletions shell/src/cmd/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use colored::*;

#[inline]
fn help(cmd: &str, descr: &str) {
println!(" {:13} {}", cmd, descr);
}

pub fn print_help() {
println!("{}", "COMMANDS".bright_yellow());
help("list", "lists various information");
println!("\nRun <command> -h for more help.\n");
}
50 changes: 50 additions & 0 deletions shell/src/cmd/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::{Cmd, Shell};
use structopt::{clap::AppSettings, StructOpt};

#[derive(Debug, StructOpt)]
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
pub enum Args {
#[structopt(about = "Lists all contract names.")]
Contracts {
#[structopt(help = "Print all contract names from the project matching that name.")]
name: Option<String>,
#[structopt(
help = "Print all contract names from every project.",
long,
short,
conflicts_with = "name"
)]
all: bool,
},
}

impl Cmd for Args {
fn run(self, shell: &mut Shell) -> eyre::Result<()> {
match self {
Args::Contracts { name, all } => {
if all {
for artifacts in shell.session.artifacts.values() {
for name in artifacts.keys() {
println!("{}", name);
}
}
} else {
if let Some(artifacts) = name
.as_ref()
.or(shell.workspace())
.map(|name| shell.session.artifacts.get(name))
.flatten()
{
for name in artifacts.keys() {
println!("{}", name);
}
} else {
println!(".");
}
}
}
}

Ok(())
}
}
15 changes: 15 additions & 0 deletions shell/src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Various forge shell commands

pub mod help;
pub mod list;
use crate::Shell;

/// A trait for forge shell commands
pub trait Cmd: structopt::StructOpt + Sized {
fn run(self, shell: &mut Shell) -> eyre::Result<()>;

fn run_str(shell: &mut Shell, args: &[String]) -> eyre::Result<()> {
let args = Self::from_iter_safe(args)?;
args.run(shell)
}
}
56 changes: 56 additions & 0 deletions shell/src/complete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use colored::*;
use rustyline::{
completion::Completer, highlight::Highlighter, hint::Hinter, CompletionType, EditMode, Editor,
};
use std::borrow::Cow;

/// Returns a new Vi edit style editor
pub fn editor() -> Editor<SolReplCompleter> {
let config = rustyline::Config::builder()
.edit_mode(EditMode::Vi)
.completion_type(CompletionType::List)
.keyseq_timeout(0) // https://github.com/kkawakam/rustyline/issues/371
.build();
Editor::with_config(config)
}
/// Type responsible to provide hints, complete, highlight
#[derive(Default)]
pub struct SolReplCompleter {
/// Stores the available command context
context: Vec<String>,
}

impl SolReplCompleter {
pub fn set(&mut self, context: Vec<String>) {
self.context = context;
}

/// Creates a new editor and register the sol repl as helper
pub fn into_editor(self) -> Editor<SolReplCompleter> {
let mut editor = editor();
editor.set_helper(Some(self));
editor
}
}

impl rustyline::validate::Validator for SolReplCompleter {}

impl Completer for SolReplCompleter {
type Candidate = String;
}

impl Highlighter for SolReplCompleter {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> Cow<'b, str> {
prompt.yellow().to_string().into()
}
}

impl Hinter for SolReplCompleter {
type Hint = String;
}

impl rustyline::Helper for SolReplCompleter {}
34 changes: 34 additions & 0 deletions shell/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! forge config

use eyre::{Context, ContextCompat};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Default)]
// Serialize, Deserialize
pub struct Config {}

impl Config {
/// Returns the path to the forge toml file at `~/forge.toml`
pub fn path() -> eyre::Result<PathBuf> {
let path = dirs_next::config_dir().wrap_err_with(|| "Failed to detect config directory")?;
let path = path.join("forge.toml");
Ok(path)
}

pub fn load_or_default() -> eyre::Result<Config> {
let path = Config::path()?;
if path.exists() {
Config::load_from(&path)
} else {
Ok(Config::default())
}
}

pub fn load_from(path: impl AsRef<Path>) -> eyre::Result<Config> {
let _config = std::fs::read(&path).wrap_err("Failed to read config file")?;

// let config = toml::from_slice(&config)?;
// Ok(config)
todo!()
}
}
97 changes: 97 additions & 0 deletions shell/src/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use crate::Shell;
use log::debug;
use solang::parser::pt::*;

/// A user input
#[derive(Debug)]
pub enum Input {
/// A known command with its shellwords
Command(Command, Vec<String>),
/// A deserialized solidity source unit
Solang(SourceUnit),
/// Unmatched input
Other(String),
}

impl Input {
/// Consumes the line
pub fn read_line(line: impl Into<String>, shell: &mut Shell) -> Option<Self> {
let line = line.into();
if line.is_empty() {
None
} else {
debug!("Readline returned {:?}", line);

shell.rl.add_history_entry(line.as_str());

if line.starts_with('#') {
// shell comment
return None
}

let words = match shellwords::split(&line) {
Ok(cmd) => cmd,
Err(err) => {
eprintln!("Error: {:?}", err);
return None
}
};
debug!("shellwords output: {:?}", words);

if words.is_empty() {
return None
}

// try to find a matching native command
if let Some(cmd) = Command::from_str(&words[0]) {
return Some(Input::Command(cmd, words))
}

// try to deserialize as supported solidity command
if let Ok(unit) = solang::parser::parse(&line, 1) {
return Some(Input::Solang(unit))
}

// return unresolved content, delegate eval to shell itself, like `msg.sender`
Some(Input::Other(line))
}
}
}

/// Various supported solang elements
#[derive(Debug)]
pub enum SolangInput {
Contract(Box<ContractDefinition>),
Function(Box<FunctionDefinition>),
Variable(Box<VariableDefinition>),
Struct(Box<StructDefinition>),
// TODO various math expressions
}

/// various sol shell commands
#[derive(Debug)]
pub enum Command {
Dump,
Load,
Help,
Quit,
Exit,
Set,
List,
Interrupt,
}

impl Command {
fn from_str(s: &str) -> Option<Command> {
match s {
"dump" => Some(Command::Dump),
"load" => Some(Command::Load),
"help" => Some(Command::Help),
"quit" => Some(Command::Quit),
"exit" => Some(Command::Exit),
"set" => Some(Command::Set),
"ls" | "list" => Some(Command::List),
_ => None,
}
}
}
Loading