Open named database OR default to in memory database if no name provided

Rearrange workspace to allow import of mentat crate in cli crate

Create store object inside repl when started for connecting to mentat

Use provided DB name to open connection in store

Accept DB name as command line arg.

Open on CLI start

Implement '.open' command to open desired DB from inside CLI
This commit is contained in:
Emily Toop 2017-05-09 16:25:37 +01:00
parent 5a6c3f6598
commit f3d39d4194
7 changed files with 313 additions and 41 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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<String>),
Query(Vec<String>),
Help(Vec<String>),
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::<Vec<_>, _, _>(many1::<Vec<_>, _>(alpha_num()), token(' '))).map(|x| {
let args: Vec<String> = x.1.iter().map(|v| v.iter().collect() ).collect();
Command::Help(args)
});
let open_parser = string(OPEN_COMMAND).and(space()).and(many1::<Vec<_>, _>(alpha_num()).and(eof())).map(|x| {
let arg: String = (x.1).0.iter().collect();
Command::Open(arg)
});
token('.')
.and(choice::<[&mut Parser<Input = _, Output = Command>; 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<String> = 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<String> = 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)
}
}
}

View file

@ -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<String>),
/// 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<String>),
}
/// 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<String> {
@ -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()
}
}

View file

@ -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::<Vec<_>>();
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

View file

@ -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<String>) -> Repl {
Repl{
store: Store::new(db_name),
}
}
/// Runs the REPL interactively.
pub fn run(&mut self) {
let mut more = false;
let mut more: Option<Command> = 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<String>) {
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<String>) {
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);
}
}
}
}
}

View file

@ -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<String>) -> 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<String>) {
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);
}
}