diff --git a/Cargo.toml b/Cargo.toml index 6cb47d11..ff694e36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ version = "0.4.0" build = "build/version.rs" [workspace] -members = [] +members = ["tools/cli"] [build-dependencies] rustc_version = "0.1.7" @@ -61,6 +61,3 @@ path = "query-translator" [dependencies.mentat_tx_parser] path = "tx-parser" - -[dependencies.mentat_cli] -path = "tools/cli" diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 67554a93..7523a9b3 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "mentat_cli" version = "0.0.1" -workspace = "../.." [lib] name = "mentat_cli" @@ -18,3 +17,16 @@ env_logger = "0.3" linefeed = "0.1" log = "0.3" tempfile = "1.1" +combine = "2.2.2" +lazy_static = "0.2.2" + +[dependencies.rusqlite] +version = "0.11" +# System sqlite might be very old. +features = ["bundled", "limits"] + +[dependencies.mentat] +path = "../.." + +[dependencies.mentat_parser_utils] +path = "../../parser-utils" diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs new file mode 100644 index 00000000..bec63486 --- /dev/null +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -0,0 +1,159 @@ +// Copyright 2017 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +use combine::{eof, many, many1, sep_by, skip_many, token, Parser}; +use combine::combinator::{choice, try}; +use combine::char::{alpha_num, space, string}; + +pub static HELP_COMMAND: &'static str = &"help"; +pub static OPEN_COMMAND: &'static str = &"open"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Command { + Transact(Vec), + Query(Vec), + Help(Vec), + Open(String), + Err(String), +} + +impl Command { + pub fn is_complete(&self) -> bool { + match self { + &Command::Query(_) | + &Command::Transact(_) => false, + _ => true + } + } +} + +pub fn command(s: &str) -> Command { + let help_parser = string(HELP_COMMAND).and(skip_many(space())).and(sep_by::, _, _>(many1::, _>(alpha_num()), token(' '))).map(|x| { + let args: Vec = x.1.iter().map(|v| v.iter().collect() ).collect(); + Command::Help(args) + }); + + let open_parser = string(OPEN_COMMAND).and(space()).and(many1::, _>(alpha_num()).and(eof())).map(|x| { + let arg: String = (x.1).0.iter().collect(); + Command::Open(arg) + }); + + token('.') + .and(choice::<[&mut Parser; 2], _> + ([&mut try(help_parser), + &mut try(open_parser),])) + .parse(s) + .map(|x| x.0) + .unwrap_or(('0', Command::Err(format!("Invalid command {:?}", s)))).1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_help_parser_multiple_args() { + let input = ".help command1 command2"; + let cmd = command(&input); + match cmd { + Command::Help(args) => { + assert_eq!(args, vec!["command1", "command2"]); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_no_args() { + let input = ".help"; + let cmd = command(&input); + match cmd { + Command::Help(args) => { + let empty: Vec = vec![]; + assert_eq!(args, empty); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_no_args_trailing_whitespace() { + let input = ".help "; + let cmd = command(&input); + match cmd { + Command::Help(args) => { + let empty: Vec = vec![]; + assert_eq!(args, empty); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_multiple_args() { + let input = ".open database1 database2"; + let cmd = command(&input); + match cmd { + Command::Err(message) => { + assert_eq!(message, format!("Invalid command {:?}", input)); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_single_arg() { + let input = ".open database1"; + let cmd = command(&input); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "database1".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_no_args() { + let input = ".open"; + let cmd = command(&input); + match cmd { + Command::Err(message) => { + assert_eq!(message, format!("Invalid command {:?}", input)); + }, + _ => assert!(false) + } + } + + #[test] + fn test_command_parser_no_dot() { + let input = "help command1 command2"; + let cmd = command(&input); + match cmd { + Command::Err(message) => { + assert_eq!(message, format!("Invalid command {:?}", input)); + }, + _ => assert!(false) + } + } + + #[test] + fn test_command_parser_invalid_cmd() { + let input = ".foo command1"; + let cmd = command(&input); + match cmd { + Command::Err(message) => { + assert_eq!(message, format!("Invalid command {:?}", input)); + }, + _ => assert!(false) + } + + } +} diff --git a/tools/cli/src/mentat_cli/input.rs b/tools/cli/src/mentat_cli/input.rs index 267b3cf4..2a9a35f0 100644 --- a/tools/cli/src/mentat_cli/input.rs +++ b/tools/cli/src/mentat_cli/input.rs @@ -8,24 +8,29 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -use std::io::{self, stdin, BufRead, BufReader}; +use std::io::{stdin}; use linefeed::Reader; use linefeed::terminal::DefaultTerminal; use self::InputResult::*; +use command_parser::{Command, command}; + /// Possible results from reading input from `InputReader` #[derive(Clone, Debug)] pub enum InputResult { - /// rusti command as input; (name, rest of line) - Command(String, Option), + /// mentat command as input; (name, rest of line) + MetaCommand(Command), /// An empty line Empty, /// Needs more input; i.e. there is an unclosed delimiter - More, + More(Command), /// End of file reached Eof, + /// Error while parsing input; a Rust parsing error will have printed out + /// error messages and therefore contain no error message. + InputError(Option), } /// Reads input from `stdin` @@ -73,14 +78,18 @@ impl InputReader { self.add_history(&line); - let res = More; + let cmd = command(&self.buffer); - match res { - More => (), - _ => self.buffer.clear(), - }; - - res + match cmd { + Command::Query(_) | + Command::Transact(_) if !cmd.is_complete() => { + More(cmd) + }, + _ => { + self.buffer.clear(); + InputResult::MetaCommand(cmd) + } + } } fn read_line(&mut self, prompt: &str) -> Option { @@ -88,7 +97,7 @@ impl InputReader { Some(ref mut r) => { r.set_prompt(prompt); r.read_line().ok().and_then(|line| line) - } + }, None => self.read_stdin() } } diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs index 41d196fd..49533170 100644 --- a/tools/cli/src/mentat_cli/lib.rs +++ b/tools/cli/src/mentat_cli/lib.rs @@ -7,16 +7,24 @@ // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. + #![crate_name = "mentat_cli"] #[macro_use] extern crate log; +#[macro_use] extern crate lazy_static; +extern crate combine; extern crate env_logger; extern crate getopts; extern crate linefeed; +extern crate rusqlite; + +extern crate mentat; use getopts::Options; +pub mod command_parser; +pub mod store; pub mod input; pub mod repl; @@ -26,6 +34,7 @@ pub fn run() -> i32 { let args = std::env::args().collect::>(); let mut opts = Options::new(); + opts.optopt("d", "", "The path to a database to open", "DATABASE"); opts.optflag("h", "help", "Print this help message and exit"); opts.optflag("v", "version", "Print version and exit"); @@ -41,12 +50,15 @@ pub fn run() -> i32 { print_version(); return 0; } + if matches.opt_present("help") { print_usage(&args[0], &opts); return 0; } - let mut repl = repl::Repl::new(); + let db_name = matches.opt_str("d"); + + let mut repl = repl::Repl::new(db_name); repl.run(); 0 diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index d7b95f58..9292fd2a 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -7,63 +7,101 @@ // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. + +use std::collections::HashMap; + +use command_parser::{Command, HELP_COMMAND, OPEN_COMMAND}; use input::{InputReader}; -use input::InputResult::{Command, Empty, More, Eof}; +use input::InputResult::{MetaCommand, Empty, More, Eof, InputError}; +use store::Store; /// Starting prompt const DEFAULT_PROMPT: &'static str = "mentat=> "; /// Prompt when further input is being read +// TODO: Should this actually reflect the current open brace? const MORE_PROMPT: &'static str = "mentat.> "; -/// Prompt when a `.block` command is in effect -const BLOCK_PROMPT: &'static str = "mentat+> "; + +lazy_static! { + static ref COMMAND_HELP: HashMap<&'static str, &'static str> = { + let mut map = HashMap::new(); + map.insert(HELP_COMMAND, "Show help for commands."); + map.insert(OPEN_COMMAND, "Open a database at path."); + map + }; +} /// Executes input and maintains state of persistent items. pub struct Repl { + store: Store, } impl Repl { /// Constructs a new `Repl`. - pub fn new() -> Repl { - Repl{} + pub fn new(db_name: Option) -> Repl { + Repl{ + store: Store::new(db_name), + } } - /// Runs the REPL interactively. pub fn run(&mut self) { - let mut more = false; + let mut more: Option = None; let mut input = InputReader::new(); loop { - let res = input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }); - // let res = if self.read_block { - // self.read_block = false; - // input.read_block_input(BLOCK_PROMPT) - // } else { - // input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }) - // }; + let res = input.read_input(if more.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT }); match res { - Command(name, args) => { - debug!("read command: {} {:?}", name, args); - - more = false; - self.handle_command(name, args); + MetaCommand(cmd) => { + debug!("read command: {:?}", cmd); + more = None; + self.handle_command(cmd); }, Empty => (), - More => { more = true; }, + More(cmd) => { more = Some(cmd); }, Eof => { if input.is_tty() { println!(""); } break; - } + }, + InputError(err) => { + if let Some(err) = err { + println!("{}", err); + } + more = None; + }, }; } } /// Runs a single command input. - fn handle_command(&mut self, cmd: String, args: Option) { - println!("{:?} {:?}", cmd, args); + fn handle_command(&mut self, cmd: Command) { + match cmd { + Command::Help(args) => self.help_command(args), + Command::Open(db) => { + self.store.open(Some(db)); + }, + Command::Err(message) => println!("{}", message), + _ => unimplemented!(), + } + } + + fn help_command(&self, args: Vec) { + if args.is_empty() { + for (cmd, msg) in COMMAND_HELP.iter() { + println!(".{} - {}", cmd, msg); + } + } else { + for arg in args { + let msg = COMMAND_HELP.get(arg.as_str()); + if msg.is_some() { + println!(".{} - {}", arg, msg.unwrap()); + } else { + println!("Unrecognised command {}", arg); + } + } + } } } diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs new file mode 100644 index 00000000..c54757df --- /dev/null +++ b/tools/cli/src/mentat_cli/store.rs @@ -0,0 +1,45 @@ +// Copyright 2017 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +use rusqlite; +use mentat::{ + new_connection, +}; + +use mentat::conn::Conn; + +pub struct Store { + handle: rusqlite::Connection, + conn: Conn, +} + +fn db_output_name(db_name: &String) -> String { + if db_name.is_empty() { "in memory db".to_string() } else { db_name.clone() } +} + +impl Store { + pub fn new(database: Option) -> Store { + let db_name = database.unwrap_or("".to_string()); + let output_name = db_output_name(&db_name); + let mut handle = new_connection(db_name).expect("Couldn't open conn."); + let conn = Conn::connect(&mut handle).expect("Couldn't open DB."); + println!("Database {:?} opened", output_name); + Store { handle, conn } + } + + pub fn open(&mut self, database: Option) { + let db_name = database.unwrap_or("".to_string()); + let output_name = db_output_name(&db_name); + self.handle = new_connection(db_name).expect("Couldn't open conn."); + self.conn = Conn::connect(&mut self.handle).expect("Couldn't open DB."); + println!("Database {:?} opened", output_name); + } + +}