* Create mentat command line.
* Create tools directory containing new crate for mentat_cli.
* Add simple cli with mentat prompt.

* Remove rustc-serialize dependency

* Open DB inside CLI (#452) (#463)

* 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

* Implement Close command to close current DB.
* Closes existing open db and opens new in memory db

* Review comment: Use `combine` to parse arguments.

Move over to using Result rather than enums with err

* Accept and parse EDN Query and Transact commands (#453) (#465)

* Parse query and transact commands

* Implement is_complete for transactions and queries

* Improve query parser. Am still not happy with it though.

There must be some way that I can retain the eof() after the `then` that means I don't have to move the skip on spaces and eof

Make in process command storing clearer.

Add comments around in process commands.
Add alternative commands for transact/t and query/q

* Address review comments r=nalexander.

* Bump rust version number.
* Use `bail` when throwing errors.
* Improve edn parser.
* Remove references to unused `more` flag.
* Improve naming of query and transact commands.

* Send queries and transactions to mentat and output the results (#466)

* Send queries and transactions to mentat and output the results

move outputting query and transaction results out of store and into repl

* Add query and transact commands to help

* Execute queries and transacts passed in at startup

* Address review comments =nalexander.

* Bump rust version number.
* Use `bail` when throwing errors.
* Improve edn parser.
* Remove references to unused `more` flag.
* Improve naming of query and transact commands.

* Execute command line args in order

* Addressing rebase issues

* Exit CLI (#457) (#484) r-rnewman

* Implement exit command for cli tool

* Address review comments r=rnewman

* Include exit commands in help

* Show schema of current DB (#487)

* Fixing rebase issues

* addressing nit

* Match updated dependencies on CLI crate and remove unused import
This commit is contained in:
Emily Toop 2017-11-21 16:56:16 +00:00 committed by GitHub
parent c600152d78
commit 55588209c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1196 additions and 3 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"

View file

@ -16,7 +16,7 @@ use rustc_version::version_matches;
/// MIN_VERSION should be changed when there's a new minimum version of rustc required
/// to build the project.
static MIN_VERSION: &'static str = ">= 1.15.1";
static MIN_VERSION: &'static str = ">= 1.17.0";
fn main() {
if !version_matches(MIN_VERSION) {

View file

@ -43,7 +43,7 @@ mod entids;
pub mod errors;
mod metadata;
mod schema;
mod types;
pub mod types;
mod internal_types;
mod upsert_resolution;
mod tx;

45
tools/cli/Cargo.toml Normal file
View file

@ -0,0 +1,45 @@
[package]
name = "mentat_cli"
version = "0.0.1"
[lib]
name = "mentat_cli"
path = "src/mentat_cli/lib.rs"
[[bin]]
name = "mentat_cli"
doc = false
test = false
[dependencies]
getopts = "0.2"
env_logger = "0.3"
linefeed = "0.1"
log = "0.3"
tempfile = "1.1"
combine = "2.2.2"
lazy_static = "0.2"
error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" }
[dependencies.rusqlite]
version = "0.12"
# System sqlite might be very old.
features = ["bundled", "limits"]
[dependencies.mentat]
path = "../.."
[dependencies.mentat_parser_utils]
path = "../../parser-utils"
[dependencies.edn]
path = "../../edn"
[dependencies.mentat_query]
path = "../../query"
[dependencies.mentat_core]
path = "../../core"
[dependencies.mentat_db]
path = "../../db"

View file

@ -0,0 +1,16 @@
// 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.
extern crate mentat_cli;
fn main() {
let status = mentat_cli::run();
std::process::exit(status);
}

View file

@ -0,0 +1,524 @@
// 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::{
any,
eof,
look_ahead,
many1,
satisfy,
sep_end_by,
token,
Parser
};
use combine::char::{
space,
spaces,
string
};
use combine::combinator::{
choice,
try
};
use errors as cli;
use edn;
pub static HELP_COMMAND: &'static str = &"help";
pub static OPEN_COMMAND: &'static str = &"open";
pub static CLOSE_COMMAND: &'static str = &"close";
pub static LONG_QUERY_COMMAND: &'static str = &"query";
pub static SHORT_QUERY_COMMAND: &'static str = &"q";
pub static SCHEMA_COMMAND: &'static str = &"schema";
pub static LONG_TRANSACT_COMMAND: &'static str = &"transact";
pub static SHORT_TRANSACT_COMMAND: &'static str = &"t";
pub static LONG_EXIT_COMMAND: &'static str = &"exit";
pub static SHORT_EXIT_COMMAND: &'static str = &"e";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Command {
Close,
Exit,
Help(Vec<String>),
Open(String),
Query(String),
Schema,
Transact(String),
}
impl Command {
/// is_complete returns true if no more input is required for the command to be successfully executed.
/// false is returned if the command is not considered valid.
/// Defaults to true for all commands except Query and Transact.
/// TODO: for query and transact commands, they will be considered complete if a parsable EDN has been entered as an argument
pub fn is_complete(&self) -> bool {
match self {
&Command::Query(ref args) |
&Command::Transact(ref args) => {
edn::parse::value(&args).is_ok()
},
&Command::Help(_) |
&Command::Open(_) |
&Command::Close |
&Command::Exit |
&Command::Schema => true
}
}
pub fn output(&self) -> String {
match self {
&Command::Query(ref args) => {
format!(".{} {}", LONG_QUERY_COMMAND, args)
},
&Command::Transact(ref args) => {
format!(".{} {}", LONG_TRANSACT_COMMAND, args)
},
&Command::Help(ref args) => {
format!(".{} {:?}", HELP_COMMAND, args)
},
&Command::Open(ref args) => {
format!(".{} {}", OPEN_COMMAND, args)
},
&Command::Close => {
format!(".{}", CLOSE_COMMAND)
},
&Command::Exit => {
format!(".{}", LONG_EXIT_COMMAND)
},
&Command::Schema => {
format!(".{}", SCHEMA_COMMAND)
},
}
}
}
pub fn command(s: &str) -> Result<Command, cli::Error> {
let arguments = || sep_end_by::<Vec<_>, _, _>(many1(satisfy(|c: char| !c.is_whitespace())), many1::<Vec<_>, _>(space())).expected("arguments");
let help_parser = string(HELP_COMMAND)
.with(spaces())
.with(arguments())
.map(|args| {
Ok(Command::Help(args.clone()))
});
let open_parser = string(OPEN_COMMAND)
.with(spaces())
.with(arguments())
.map(|args| {
if args.len() < 1 {
bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string()));
}
if args.len() > 1 {
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])));
}
Ok(Command::Open(args[0].clone()))
});
let no_arg_parser = || arguments()
.skip(spaces())
.skip(eof());
let close_parser = string(CLOSE_COMMAND)
.with(no_arg_parser())
.map(|args| {
if !args.is_empty() {
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) );
}
Ok(Command::Close)
});
let schema_parser = string(SCHEMA_COMMAND)
.with(no_arg_parser())
.map(|args| {
if !args.is_empty() {
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) );
}
Ok(Command::Schema)
});
let exit_parser = try(string(LONG_EXIT_COMMAND)).or(try(string(SHORT_EXIT_COMMAND)))
.with(no_arg_parser())
.map(|args| {
if !args.is_empty() {
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) );
}
Ok(Command::Exit)
});
let edn_arg_parser = || spaces()
.with(look_ahead(string("[").or(string("{")))
.with(many1::<Vec<_>, _>(try(any())))
.and_then(|args| -> Result<String, cli::Error> {
Ok(args.iter().collect())
})
);
let query_parser = try(string(LONG_QUERY_COMMAND)).or(try(string(SHORT_QUERY_COMMAND)))
.with(edn_arg_parser())
.map(|x| {
Ok(Command::Query(x))
});
let transact_parser = try(string(LONG_TRANSACT_COMMAND)).or(try(string(SHORT_TRANSACT_COMMAND)))
.with(edn_arg_parser())
.map( |x| {
Ok(Command::Transact(x))
});
spaces()
.skip(token('.'))
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 7], _>
([&mut try(help_parser),
&mut try(open_parser),
&mut try(close_parser),
&mut try(exit_parser),
&mut try(query_parser),
&mut try(schema_parser),
&mut try(transact_parser)]))
.parse(s)
.unwrap_or((Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()), "")).0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_help_parser_multiple_args() {
let input = ".help command1 command2";
let cmd = command(&input).expect("Expected help command");
match cmd {
Command::Help(args) => {
assert_eq!(args, vec!["command1", "command2"]);
},
_ => assert!(false)
}
}
#[test]
fn test_help_parser_dot_arg() {
let input = ".help .command1";
let cmd = command(&input).expect("Expected help command");
match cmd {
Command::Help(args) => {
assert_eq!(args, vec![".command1"]);
},
_ => assert!(false)
}
}
#[test]
fn test_help_parser_no_args() {
let input = ".help";
let cmd = command(&input).expect("Expected help command");
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).expect("Expected help command");
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 err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), "Unrecognized argument \"database2\"");
}
#[test]
fn test_open_parser_single_arg() {
let input = ".open database1";
let cmd = command(&input).expect("Expected open command");
match cmd {
Command::Open(arg) => {
assert_eq!(arg, "database1".to_string());
},
_ => assert!(false)
}
}
#[test]
fn test_open_parser_path_arg() {
let input = ".open /path/to/my.db";
let cmd = command(&input).expect("Expected open command");
match cmd {
Command::Open(arg) => {
assert_eq!(arg, "/path/to/my.db".to_string());
},
_ => assert!(false)
}
}
#[test]
fn test_open_parser_file_arg() {
let input = ".open my.db";
let cmd = command(&input).expect("Expected open command");
match cmd {
Command::Open(arg) => {
assert_eq!(arg, "my.db".to_string());
},
_ => assert!(false)
}
}
#[test]
fn test_open_parser_no_args() {
let input = ".open";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), "Missing required argument");
}
#[test]
fn test_open_parser_no_args_trailing_whitespace() {
let input = ".open ";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), "Missing required argument");
}
#[test]
fn test_close_parser_with_args() {
let input = ".close arg1";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_close_parser_no_args() {
let input = ".close";
let cmd = command(&input).expect("Expected close command");
match cmd {
Command::Close => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_close_parser_no_args_trailing_whitespace() {
let input = ".close ";
let cmd = command(&input).expect("Expected close command");
match cmd {
Command::Close => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_exit_parser_with_args() {
let input = ".exit arg1";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_exit_parser_no_args() {
let input = ".exit";
let cmd = command(&input).expect("Expected exit command");
match cmd {
Command::Exit => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_exit_parser_no_args_trailing_whitespace() {
let input = ".exit ";
let cmd = command(&input).expect("Expected exit command");
match cmd {
Command::Exit => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_exit_parser_short_command() {
let input = ".e";
let cmd = command(&input).expect("Expected exit command");
match cmd {
Command::Exit => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_schema_parser_with_args() {
let input = ".schema arg1";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_schema_parser_no_args() {
let input = ".schema";
let cmd = command(&input).expect("Expected schema command");
match cmd {
Command::Schema => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_schema_parser_no_args_trailing_whitespace() {
let input = ".schema ";
let cmd = command(&input).expect("Expected schema command");
match cmd {
Command::Schema => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_query_parser_complete_edn() {
let input = ".q [:find ?x :where [?x foo/bar ?y]]";
let cmd = command(&input).expect("Expected query command");
match cmd {
Command::Query(edn) => assert_eq!(edn, "[:find ?x :where [?x foo/bar ?y]]"),
_ => assert!(false)
}
}
#[test]
fn test_query_parser_alt_query_command() {
let input = ".query [:find ?x :where [?x foo/bar ?y]]";
let cmd = command(&input).expect("Expected query command");
match cmd {
Command::Query(edn) => assert_eq!(edn, "[:find ?x :where [?x foo/bar ?y]]"),
_ => assert!(false)
}
}
#[test]
fn test_query_parser_incomplete_edn() {
let input = ".q [:find ?x\r\n";
let cmd = command(&input).expect("Expected query command");
match cmd {
Command::Query(edn) => assert_eq!(edn, "[:find ?x\r\n"),
_ => assert!(false)
}
}
#[test]
fn test_query_parser_empty_edn() {
let input = ".q {}";
let cmd = command(&input).expect("Expected query command");
match cmd {
Command::Query(edn) => assert_eq!(edn, "{}"),
_ => assert!(false)
}
}
#[test]
fn test_query_parser_no_edn() {
let input = ".q ";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_query_parser_invalid_start_char() {
let input = ".q :find ?x";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_transact_parser_complete_edn() {
let input = ".t [[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]";
let cmd = command(&input).expect("Expected transact command");
match cmd {
Command::Transact(edn) => assert_eq!(edn, "[[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"),
_ => assert!(false)
}
}
#[test]
fn test_transact_parser_alt_command() {
let input = ".transact [[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]";
let cmd = command(&input).expect("Expected transact command");
match cmd {
Command::Transact(edn) => assert_eq!(edn, "[[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"),
_ => assert!(false)
}
}
#[test]
fn test_transact_parser_incomplete_edn() {
let input = ".t {\r\n";
let cmd = command(&input).expect("Expected transact command");
match cmd {
Command::Transact(edn) => assert_eq!(edn, "{\r\n"),
_ => assert!(false)
}
}
#[test]
fn test_transact_parser_empty_edn() {
let input = ".t {}";
let cmd = command(&input).expect("Expected transact command");
match cmd {
Command::Transact(edn) => assert_eq!(edn, "{}"),
_ => assert!(false)
}
}
#[test]
fn test_transact_parser_no_edn() {
let input = ".t ";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_transact_parser_invalid_start_char() {
let input = ".t :db/add \"s\" :db/ident :foo/uuid";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_parser_preceeding_trailing_whitespace() {
let input = " .close ";
let cmd = command(&input).expect("Expected close command");
match cmd {
Command::Close => assert!(true),
_ => assert!(false)
}
}
#[test]
fn test_command_parser_no_dot() {
let input = "help command1 command2";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
#[test]
fn test_command_parser_invalid_cmd() {
let input = ".foo command1";
let err = command(&input).expect_err("Expected an error");
assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
}
}

View file

@ -0,0 +1,36 @@
// Copyright 2016 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.
#![allow(dead_code)]
use rusqlite;
use mentat::errors as mentat;
error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
}
foreign_links {
Rusqlite(rusqlite::Error);
}
links {
MentatError(mentat::Error, mentat::ErrorKind);
}
errors {
CommandParse(message: String) {
description("An error occured parsing the entered command")
display("{}", message)
}
}
}

View file

@ -0,0 +1,157 @@
// 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 std::io::stdin;
use linefeed::Reader;
use linefeed::terminal::DefaultTerminal;
use self::InputResult::*;
use command_parser::{
Command,
command
};
use errors as cli;
/// 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.> ";
/// Possible results from reading input from `InputReader`
#[derive(Clone, Debug)]
pub enum InputResult {
/// mentat command as input; (name, rest of line)
MetaCommand(Command),
/// An empty line
Empty,
/// Needs more input
More,
/// End of file reached
Eof,
}
/// Reads input from `stdin`
pub struct InputReader {
buffer: String,
reader: Option<Reader<DefaultTerminal>>,
in_process_cmd: Option<Command>,
}
impl InputReader {
/// Constructs a new `InputReader` reading from `stdin`.
pub fn new() -> InputReader {
let r = match Reader::new("mentat") {
Ok(mut r) => {
r.set_word_break_chars(" \t\n!\"#$%&'()*+,-./:;<=>?@[\\]^`");
Some(r)
}
Err(_) => None
};
InputReader{
buffer: String::new(),
reader: r,
in_process_cmd: None,
}
}
/// Returns whether the `InputReader` is reading from a TTY.
pub fn is_tty(&self) -> bool {
self.reader.is_some()
}
/// Reads a single command, item, or statement from `stdin`.
/// Returns `More` if further input is required for a complete result.
/// In this case, the input received so far is buffered internally.
pub fn read_input(&mut self) -> Result<InputResult, cli::Error> {
let prompt = if self.in_process_cmd.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT };
let line = match self.read_line(prompt) {
Some(s) => s,
None => return Ok(Eof),
};
self.buffer.push_str(&line);
if self.buffer.is_empty() {
return Ok(Empty);
}
self.add_history(&line);
// if we have a command in process (i.e. in incomplete query or transaction),
// then we already know which type of command it is and so we don't need to parse the
// command again, only the content, which we do later.
// Therefore, we add the newly read in line to the existing command args.
// If there is no in process command, we parse the read in line as a new command.
let cmd = match &self.in_process_cmd {
&Some(Command::Query(ref args)) => {
Command::Query(args.clone() + " " + &line)
},
&Some(Command::Transact(ref args)) => {
Command::Transact(args.clone() + " " + &line)
},
_ => {
try!(command(&self.buffer))
}
};
match cmd {
Command::Query(_) |
Command::Transact(_) if !cmd.is_complete() => {
// a query or transact is complete if it contains a valid edn.
// if the command is not complete, ask for more from the repl and remember
// which type of command we've found here.
self.in_process_cmd = Some(cmd);
Ok(More)
},
_ => {
self.buffer.clear();
self.in_process_cmd = None;
Ok(InputResult::MetaCommand(cmd))
}
}
}
fn read_line(&mut self, prompt: &str) -> Option<String> {
match self.reader {
Some(ref mut r) => {
r.set_prompt(prompt);
r.read_line().ok().and_then(|line| line)
},
None => self.read_stdin()
}
}
fn read_stdin(&self) -> Option<String> {
let mut s = String::new();
match stdin().read_line(&mut s) {
Ok(0) | Err(_) => None,
Ok(_) => Some(s)
}
}
fn add_history(&mut self, line: &str) {
if let Some(ref mut r) = self.reader {
r.add_history(line.to_owned());
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
}
}

View file

@ -0,0 +1,121 @@
// 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.
#![crate_name = "mentat_cli"]
#[macro_use] extern crate log;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate error_chain;
extern crate combine;
extern crate env_logger;
extern crate getopts;
extern crate linefeed;
extern crate rusqlite;
extern crate mentat;
extern crate edn;
extern crate mentat_query;
extern crate mentat_core;
extern crate mentat_db;
use getopts::Options;
pub mod command_parser;
pub mod store;
pub mod input;
pub mod repl;
pub mod errors;
pub fn run() -> i32 {
env_logger::init().unwrap();
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.optmulti("q", "query", "Execute a query on startup. Queries are executed after any transacts.", "QUERY");
opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT");
opts.optflag("v", "version", "Print version and exit");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(e) => {
println!("{}: {}", args[0], e);
return 1;
}
};
if matches.opt_present("version") {
print_version();
return 0;
}
if matches.opt_present("help") {
print_usage(&args[0], &opts);
return 0;
}
let mut last_arg: Option<&str> = None;
let cmds:Vec<command_parser::Command> = args.iter().filter_map(|arg| {
match last_arg {
Some("-d") => {
last_arg = None;
Some(command_parser::Command::Open(arg.clone()))
},
Some("-q") => {
last_arg = None;
Some(command_parser::Command::Query(arg.clone()))
},
Some("-t") => {
last_arg = None;
Some(command_parser::Command::Transact(arg.clone()))
},
Some(_) |
None => {
last_arg = Some(&arg);
None
},
}
}).collect();
let repl = repl::Repl::new();
if repl.is_ok() {
repl.unwrap().run(Some(cmds));
} else {
println!("{}", repl.err().unwrap());
}
0
}
/// Returns a version string.
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn print_usage(arg0: &str, opts: &Options) {
print!("{}", opts.usage(&format!(
"Usage: {} [OPTIONS] [FILE]", arg0)));
}
fn print_version() {
println!("mentat {}", version());
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
}
}

View file

@ -0,0 +1,226 @@
// 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 std::collections::HashMap;
use std::process;
use mentat::query::QueryResults;
use mentat_core::TypedValue;
use command_parser::{
Command,
HELP_COMMAND,
OPEN_COMMAND,
LONG_QUERY_COMMAND,
SHORT_QUERY_COMMAND,
SCHEMA_COMMAND,
LONG_TRANSACT_COMMAND,
SHORT_TRANSACT_COMMAND,
LONG_EXIT_COMMAND,
SHORT_EXIT_COMMAND,
};
use input::InputReader;
use input::InputResult::{
MetaCommand,
Empty,
More,
Eof
};
use store::{
Store,
db_output_name
};
lazy_static! {
static ref COMMAND_HELP: HashMap<&'static str, &'static str> = {
let mut map = HashMap::new();
map.insert(LONG_EXIT_COMMAND, "Close the current database and exit the REPL.");
map.insert(SHORT_EXIT_COMMAND, "Shortcut for `.exit`. Close the current database and exit the REPL.");
map.insert(HELP_COMMAND, "Show help for commands.");
map.insert(OPEN_COMMAND, "Open a database at path.");
map.insert(LONG_QUERY_COMMAND, "Execute a query against the current open database.");
map.insert(SHORT_QUERY_COMMAND, "Shortcut for `.query`. Execute a query against the current open database.");
map.insert(SCHEMA_COMMAND, "Output the schema for the current open database.");
map.insert(LONG_TRANSACT_COMMAND, "Execute a transact against the current open database.");
map.insert(SHORT_TRANSACT_COMMAND, "Shortcut for `.transact`. Execute a transact against the current open database.");
map
};
}
/// Executes input and maintains state of persistent items.
pub struct Repl {
store: Store
}
impl Repl {
/// Constructs a new `Repl`.
pub fn new() -> Result<Repl, String> {
let store = Store::new(None).map_err(|e| e.to_string())?;
Ok(Repl{
store: store,
})
}
/// Runs the REPL interactively.
pub fn run(&mut self, startup_commands: Option<Vec<Command>>) {
let mut input = InputReader::new();
if let Some(cmds) = startup_commands {
for command in cmds.iter() {
println!("{}", command.output());
self.handle_command(command.clone());
}
}
loop {
let res = input.read_input();
match res {
Ok(MetaCommand(cmd)) => {
debug!("read command: {:?}", cmd);
self.handle_command(cmd);
},
Ok(Empty) |
Ok(More) => (),
Ok(Eof) => {
if input.is_tty() {
println!("");
}
break;
},
Err(e) => println!("{}", e.to_string()),
}
}
}
/// Runs a single command input.
fn handle_command(&mut self, cmd: Command) {
match cmd {
Command::Help(args) => self.help_command(args),
Command::Open(db) => {
match self.store.open(Some(db.clone())) {
Ok(_) => println!("Database {:?} opened", db_output_name(&db)),
Err(e) => println!("{}", e.to_string())
};
},
Command::Close => self.close(),
Command::Query(query) => self.execute_query(query),
Command::Schema => {
let edn = self.store.fetch_schema();
match edn.to_pretty(120) {
Ok(s) => println!("{}", s),
Err(e) => println!("{}", e)
};
}
Command::Transact(transaction) => self.execute_transact(transaction),
Command::Exit => {
self.close();
println!("Exiting...");
process::exit(0);
}
}
}
fn close(&mut self) {
let old_db_name = self.store.db_name.clone();
match self.store.close() {
Ok(_) => println!("Database {:?} closed", db_output_name(&old_db_name)),
Err(e) => println!("{}", e)
};
}
fn help_command(&self, args: Vec<String>) {
if args.is_empty() {
for (cmd, msg) in COMMAND_HELP.iter() {
println!(".{} - {}", cmd, msg);
}
} else {
for mut arg in args {
if arg.chars().nth(0).unwrap() == '.' {
arg.remove(0);
}
let msg = COMMAND_HELP.get(arg.as_str());
if msg.is_some() {
println!(".{} - {}", arg, msg.unwrap());
} else {
println!("Unrecognised command {}", arg);
}
}
}
}
pub fn execute_query(&self, query: String) {
let results = match self.store.query(query){
Result::Ok(vals) => {
vals
},
Result::Err(err) => return println!("{:?}.", err),
};
if results.is_empty() {
println!("No results found.")
}
let mut output:String = String::new();
match results {
QueryResults::Scalar(Some(val)) => {
output.push_str(&self.typed_value_as_string(val) );
},
QueryResults::Tuple(Some(vals)) => {
for val in vals {
output.push_str(&format!("{}\t", self.typed_value_as_string(val)));
}
},
QueryResults::Coll(vv) => {
for val in vv {
output.push_str(&format!("{}\n", self.typed_value_as_string(val)));
}
},
QueryResults::Rel(vvv) => {
for vv in vvv {
for v in vv {
output.push_str(&format!("{}\t", self.typed_value_as_string(v)));
}
output.push_str("\n");
}
},
_ => output.push_str(&format!("No results found."))
}
println!("\n{}", output);
}
pub fn execute_transact(&mut self, transaction: String) {
match self.store.transact(transaction) {
Result::Ok(report) => println!("{:?}", report),
Result::Err(err) => println!("{:?}.", err),
}
}
fn typed_value_as_string(&self, value: TypedValue) -> String {
match value {
TypedValue::Boolean(b) => if b { "true".to_string() } else { "false".to_string() },
TypedValue::Double(d) => format!("{}", d),
TypedValue::Instant(i) => format!("{}", i),
TypedValue::Keyword(k) => format!("{}", k),
TypedValue::Long(l) => format!("{}", l),
TypedValue::Ref(r) => format!("{}", r),
TypedValue::String(s) => format!("{:?}", s.to_string()),
TypedValue::Uuid(u) => format!("{}", u),
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
}
}

View file

@ -0,0 +1,68 @@
// 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 edn;
use errors as cli;
use mentat::{
new_connection,
};
use mentat::query::QueryResults;
use mentat::conn::Conn;
use mentat_db::types::TxReport;
pub struct Store {
handle: rusqlite::Connection,
conn: Conn,
pub db_name: String,
}
pub 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>) -> Result<Store, cli::Error> {
let db_name = database.unwrap_or("".to_string());
let mut handle = try!(new_connection(&db_name));
let conn = try!(Conn::connect(&mut handle));
Ok(Store { handle, conn, db_name })
}
pub fn open(&mut self, database: Option<String>) -> Result<(), cli::Error> {
self.db_name = database.unwrap_or("".to_string());
self.handle = try!(new_connection(&self.db_name));
self.conn = try!(Conn::connect(&mut self.handle));
Ok(())
}
pub fn close(&mut self) -> Result<(), cli::Error> {
self.db_name = "".to_string();
self.open(None)
}
pub fn query(&self, query: String) -> Result<QueryResults, cli::Error> {
Ok(self.conn.q_once(&self.handle, &query, None)?)
}
pub fn transact(&mut self, transaction: String) -> Result<TxReport, cli::Error> {
Ok(self.conn.transact(&mut self.handle, &transaction)?)
}
pub fn fetch_schema(&self) -> edn::Value {
self.conn.current_schema().to_edn_value()
}
}