* 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
This commit is contained in:
parent
b76dd74108
commit
4627a2f993
10 changed files with 818 additions and 53 deletions
|
@ -11,7 +11,7 @@ version = "0.4.0"
|
|||
build = "build/version.rs"
|
||||
|
||||
[workspace]
|
||||
members = []
|
||||
members = ["tools/cli"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.1.7"
|
||||
|
@ -66,4 +66,4 @@ path = "tx-parser"
|
|||
path = "tools/cli"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
debug = true (#452) (#463)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
[package]
|
||||
name = "mentat_cli"
|
||||
version = "0.0.1"
|
||||
workspace = "../.."
|
||||
|
||||
[lib]
|
||||
name = "mentat_cli"
|
||||
|
@ -18,3 +17,29 @@ env_logger = "0.3"
|
|||
linefeed = "0.1"
|
||||
log = "0.3"
|
||||
tempfile = "1.1"
|
||||
combine = "2.2.2"
|
||||
lazy_static = "0.2.2"
|
||||
error-chain = "0.8.1"
|
||||
|
||||
[dependencies.rusqlite]
|
||||
version = "0.11"
|
||||
# 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"
|
||||
|
|
428
tools/cli/src/mentat_cli/command_parser.rs
Normal file
428
tools/cli/src/mentat_cli/command_parser.rs
Normal file
|
@ -0,0 +1,428 @@
|
|||
// 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,
|
||||
parser,
|
||||
satisfy,
|
||||
sep_end_by,
|
||||
token,
|
||||
Parser
|
||||
};
|
||||
use combine::char::{
|
||||
space,
|
||||
spaces,
|
||||
string
|
||||
};
|
||||
use combine::combinator::{
|
||||
choice,
|
||||
try
|
||||
};
|
||||
|
||||
use combine::primitives::Consumed;
|
||||
|
||||
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 LONG_TRANSACT_COMMAND: &'static str = &"transact";
|
||||
pub static SHORT_TRANSACT_COMMAND: &'static str = &"t";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Command {
|
||||
Transact(String),
|
||||
Query(String),
|
||||
Help(Vec<String>),
|
||||
Open(String),
|
||||
Close,
|
||||
}
|
||||
|
||||
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 => 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 close_parser = string(CLOSE_COMMAND)
|
||||
.with(arguments())
|
||||
.skip(spaces())
|
||||
.skip(eof())
|
||||
.map(|args| {
|
||||
if args.len() > 0 {
|
||||
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) );
|
||||
}
|
||||
Ok(Command::Close)
|
||||
});
|
||||
|
||||
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>>; 5], _>
|
||||
([&mut try(help_parser),
|
||||
&mut try(open_parser),
|
||||
&mut try(close_parser),
|
||||
&mut try(query_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_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));
|
||||
}
|
||||
}
|
36
tools/cli/src/mentat_cli/errors.rs
Normal file
36
tools/cli/src/mentat_cli/errors.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,21 +8,34 @@
|
|||
// 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
|
||||
};
|
||||
|
||||
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 {
|
||||
/// 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
|
||||
/// Needs more input
|
||||
More,
|
||||
/// End of file reached
|
||||
Eof,
|
||||
|
@ -32,6 +45,7 @@ pub enum InputResult {
|
|||
pub struct InputReader {
|
||||
buffer: String,
|
||||
reader: Option<Reader<DefaultTerminal>>,
|
||||
in_process_cmd: Option<Command>,
|
||||
}
|
||||
|
||||
impl InputReader {
|
||||
|
@ -48,6 +62,7 @@ impl InputReader {
|
|||
InputReader{
|
||||
buffer: String::new(),
|
||||
reader: r,
|
||||
in_process_cmd: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,28 +74,53 @@ impl InputReader {
|
|||
/// 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, prompt: &str) -> InputResult {
|
||||
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 Eof,
|
||||
None => return Ok(Eof),
|
||||
};
|
||||
|
||||
self.buffer.push_str(&line);
|
||||
|
||||
if self.buffer.is_empty() {
|
||||
return Empty;
|
||||
return Ok(Empty);
|
||||
}
|
||||
|
||||
self.add_history(&line);
|
||||
|
||||
let res = More;
|
||||
|
||||
match res {
|
||||
More => (),
|
||||
_ => self.buffer.clear(),
|
||||
// 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))
|
||||
}
|
||||
};
|
||||
|
||||
res
|
||||
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> {
|
||||
|
@ -88,7 +128,7 @@ impl InputReader {
|
|||
Some(ref mut r) => {
|
||||
r.set_prompt(prompt);
|
||||
r.read_line().ok().and_then(|line| line)
|
||||
}
|
||||
},
|
||||
None => self.read_stdin()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,32 @@
|
|||
// 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();
|
||||
|
@ -26,7 +40,10 @@ 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.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..]) {
|
||||
|
@ -41,13 +58,42 @@ 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();
|
||||
repl.run();
|
||||
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
|
||||
}
|
||||
|
|
|
@ -7,63 +7,192 @@
|
|||
// 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 input::{InputReader};
|
||||
use input::InputResult::{Command, Empty, More, Eof};
|
||||
|
||||
/// Starting prompt
|
||||
const DEFAULT_PROMPT: &'static str = "mentat=> ";
|
||||
/// Prompt when further input is being read
|
||||
const MORE_PROMPT: &'static str = "mentat.> ";
|
||||
/// Prompt when a `.block` command is in effect
|
||||
const BLOCK_PROMPT: &'static str = "mentat+> ";
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mentat::query::QueryResults;
|
||||
use mentat_core::TypedValue;
|
||||
|
||||
use command_parser::{
|
||||
Command,
|
||||
HELP_COMMAND,
|
||||
OPEN_COMMAND,
|
||||
LONG_QUERY_COMMAND,
|
||||
SHORT_QUERY_COMMAND,
|
||||
LONG_TRANSACT_COMMAND,
|
||||
SHORT_TRANSACT_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(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(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() -> Repl {
|
||||
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) {
|
||||
let mut more = false;
|
||||
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(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();
|
||||
|
||||
match res {
|
||||
Command(name, args) => {
|
||||
debug!("read command: {} {:?}", name, args);
|
||||
|
||||
more = false;
|
||||
self.handle_command(name, args);
|
||||
Ok(MetaCommand(cmd)) => {
|
||||
debug!("read command: {:?}", cmd);
|
||||
self.handle_command(cmd);
|
||||
},
|
||||
Empty => (),
|
||||
More => { more = true; },
|
||||
Eof => {
|
||||
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: 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) => {
|
||||
match self.store.open(Some(db.clone())) {
|
||||
Ok(_) => println!("Database {:?} opened", db_output_name(&db)),
|
||||
Err(e) => println!("{}", e.to_string())
|
||||
};
|
||||
},
|
||||
Command::Close => {
|
||||
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.to_string())
|
||||
};
|
||||
},
|
||||
Command::Query(query) => self.execute_query(query),
|
||||
Command::Transact(transaction) => self.execute_transact(transaction),
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
61
tools/cli/src/mentat_cli/store.rs
Normal file
61
tools/cli/src/mentat_cli/store.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
// 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 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(try!(self.conn.q_once(&self.handle, &query, None)))
|
||||
}
|
||||
|
||||
pub fn transact(&mut self, transaction: String) -> Result<TxReport, cli::Error> {
|
||||
Ok(try!(self.conn.transact(&mut self.handle, &transaction)))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue