Implement new REPL and remove old impls
parent
fff5780586
commit
fddb24ad0b
@ -0,0 +1,30 @@
|
||||
skysh 0.8.0
|
||||
Sayan N. <ohsayan@outlook.com>
|
||||
The Skytable interactive shell (skysh)
|
||||
|
||||
USAGE:
|
||||
skysh [OPTIONS]
|
||||
|
||||
FLAGS:
|
||||
--help Diplays this help message
|
||||
--version Displays the shell version
|
||||
|
||||
OPTIONS:
|
||||
--endpoint Set the endpoint for the connection
|
||||
--user Set the user for this client session
|
||||
--password Set the password for this client session
|
||||
--tls-cert Set the TLS certificate to use (for TLS endpoints)
|
||||
|
||||
NOTES:
|
||||
- When no endpoint is specified, skysh will attempt to connect to the default
|
||||
TCP endpoint `tcp@127.0.0.1:2003`
|
||||
- When no user is specified, skysh will attempt to authenticate as root
|
||||
- All connections need an username and password. If this is not provided
|
||||
via arguments, it will be asked for interactively
|
||||
- Endpoints are specified using the Skytable endpoint syntax. For example,
|
||||
the default TCP endpoint is `tcp@127.0.0.1:2003` while the default TLS
|
||||
endpoint is `tls@127.0.0.1:2004`
|
||||
- If you choose to use a TLS endpoint, you must provide a certificate.
|
||||
Failing to do so will throw an error, as expected
|
||||
- All history is stored in the `.sky_history` file. If you wish to delete
|
||||
it, simply remove the file
|
@ -0,0 +1,16 @@
|
||||
Welcome to the Skytable Interactive shell (REPL environment).
|
||||
|
||||
Here are a few tips to help you get started:
|
||||
- Skytable uses its own query language called BlueQL. It is mostly like SQL
|
||||
but with some important changes for security
|
||||
- Since BlueQL doesn't need a query terminator ';' you do not need to use it
|
||||
here for running queries
|
||||
- You might be surprised to see that you can use literals in this REPL while
|
||||
Skytable does not allow the use of literals for security concerns. This is
|
||||
because whenever you run a query, the REPL turns it into a parameterized query.
|
||||
- You can also run some `skysh` specific commands:
|
||||
- `!help` displays this help message
|
||||
- `clear` clears the terminal screen
|
||||
- `exit` exits the REPL session
|
||||
|
||||
Now, it's time to get querying!
|
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Created on Wed Nov 15 2023
|
||||
*
|
||||
* This file is a part of Skytable
|
||||
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
|
||||
* NoSQL database written by Sayan Nandan ("the Author") with the
|
||||
* vision to provide flexibility in data modelling without compromising
|
||||
* on performance, queryability or scalability.
|
||||
*
|
||||
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use {
|
||||
crate::error::{CliError, CliResult},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEvent},
|
||||
terminal,
|
||||
},
|
||||
std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
env, fs,
|
||||
io::{self, Write},
|
||||
process::exit,
|
||||
},
|
||||
};
|
||||
|
||||
const TXT_HELP: &str = include_str!("../help_text/help");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientConfig {
|
||||
pub kind: ClientConfigKind,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
pub fn new(kind: ClientConfigKind, username: String, password: String) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientConfigKind {
|
||||
Tcp(String, u16),
|
||||
Tls(String, u16, String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Task {
|
||||
HelpMessage(String),
|
||||
OpenShell(ClientConfig),
|
||||
}
|
||||
|
||||
enum TaskInner {
|
||||
HelpMsg(String),
|
||||
OpenShell(HashMap<String, String>),
|
||||
}
|
||||
|
||||
fn load_env() -> CliResult<TaskInner> {
|
||||
let mut args = HashMap::new();
|
||||
let mut it = env::args().skip(1).into_iter();
|
||||
while let Some(arg) = it.next() {
|
||||
let (arg, arg_val) = match arg.as_str() {
|
||||
"--help" => return Ok(TaskInner::HelpMsg(TXT_HELP.into())),
|
||||
"--version" => return Ok(TaskInner::HelpMsg(format!("skysh v{}", libsky::VERSION))),
|
||||
_ if arg.starts_with("--") => match it.next() {
|
||||
Some(arg_val) => (arg, arg_val),
|
||||
None => {
|
||||
// self contained?
|
||||
let split: Vec<&str> = arg.split("=").collect();
|
||||
if split.len() != 2 {
|
||||
return Err(CliError::ArgsErr(format!("expected value for {arg}")));
|
||||
}
|
||||
(split[0].into(), split[1].into())
|
||||
}
|
||||
},
|
||||
unknown_arg => {
|
||||
return Err(CliError::ArgsErr(format!(
|
||||
"unknown argument: {unknown_arg}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
match args.entry(arg) {
|
||||
Entry::Occupied(oe) => {
|
||||
return Err(CliError::ArgsErr(format!(
|
||||
"found duplicate values for {}",
|
||||
oe.key()
|
||||
)))
|
||||
}
|
||||
Entry::Vacant(ve) => {
|
||||
ve.insert(arg_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(TaskInner::OpenShell(args))
|
||||
}
|
||||
|
||||
pub fn parse() -> CliResult<Task> {
|
||||
let mut args = match load_env()? {
|
||||
TaskInner::HelpMsg(msg) => return Ok(Task::HelpMessage(msg)),
|
||||
TaskInner::OpenShell(args) => args,
|
||||
};
|
||||
let endpoint = match args.remove("--endpoint") {
|
||||
None => ClientConfigKind::Tcp("127.0.0.1".into(), 2003),
|
||||
Some(ep) => {
|
||||
// should be in the format protocol@host:port
|
||||
let proto_host_port: Vec<&str> = ep.split("@").collect();
|
||||
if proto_host_port.len() != 2 {
|
||||
return Err(CliError::ArgsErr("invalid value for --endpoint".into()));
|
||||
}
|
||||
let (protocol, host_port) = (proto_host_port[0], proto_host_port[1]);
|
||||
let host_port: Vec<&str> = host_port.split(":").collect();
|
||||
if host_port.len() != 2 {
|
||||
return Err(CliError::ArgsErr("invalid value for --endpoint".into()));
|
||||
}
|
||||
let (host, port) = (host_port[0], host_port[1]);
|
||||
let port = match port.parse::<u16>() {
|
||||
Ok(port) => port,
|
||||
Err(e) => {
|
||||
return Err(CliError::ArgsErr(format!(
|
||||
"invalid value for endpoint port. {e}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let tls_cert = args.remove("--tls-cert");
|
||||
match protocol {
|
||||
"tcp" => {
|
||||
// TODO(@ohsayan): warn!
|
||||
ClientConfigKind::Tcp(host.into(), port)
|
||||
}
|
||||
"tls" => {
|
||||
// we need a TLS cert
|
||||
match tls_cert {
|
||||
Some(path) => {
|
||||
let cert = fs::read_to_string(path)?;
|
||||
ClientConfigKind::Tls(host.into(), port, cert)
|
||||
}
|
||||
None => {
|
||||
return Err(CliError::ArgsErr(format!(
|
||||
"must provide TLS cert when using TLS endpoint"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(CliError::ArgsErr(format!(
|
||||
"unknown protocol scheme `{protocol}`"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let username = match args.remove("--user") {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
// default
|
||||
"root".into()
|
||||
}
|
||||
};
|
||||
let password = match args.remove("--password") {
|
||||
Some(p) => p,
|
||||
None => read_password("Enter password: ")?,
|
||||
};
|
||||
if args.is_empty() {
|
||||
Ok(Task::OpenShell(ClientConfig::new(
|
||||
endpoint, username, password,
|
||||
)))
|
||||
} else {
|
||||
Err(CliError::ArgsErr(format!("found unknown arguments")))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_password(prompt: &str) -> Result<String, std::io::Error> {
|
||||
terminal::enable_raw_mode()?;
|
||||
print!("{prompt}");
|
||||
io::stdout().flush()?;
|
||||
let mut password = String::new();
|
||||
loop {
|
||||
match event::read()? {
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: event::KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
terminal::disable_raw_mode()?;
|
||||
println!();
|
||||
exit(0x00)
|
||||
}
|
||||
Event::Key(KeyEvent { code, .. }) => match code {
|
||||
KeyCode::Backspace => {
|
||||
let _ = password.pop();
|
||||
}
|
||||
KeyCode::Char(c) => password.push(c),
|
||||
KeyCode::Enter => break,
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
terminal::disable_raw_mode()?;
|
||||
println!();
|
||||
Ok(password)
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Created on Wed Nov 15 2023
|
||||
*
|
||||
* This file is a part of Skytable
|
||||
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
|
||||
* NoSQL database written by Sayan Nandan ("the Author") with the
|
||||
* vision to provide flexibility in data modelling without compromising
|
||||
* on performance, queryability or scalability.
|
||||
*
|
||||
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use core::fmt;
|
||||
|
||||
pub type CliResult<T> = Result<T, CliError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CliError {
|
||||
QueryError(String),
|
||||
ArgsErr(String),
|
||||
ClientError(skytable::error::Error),
|
||||
IoError(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<skytable::error::Error> for CliError {
|
||||
fn from(cle: skytable::error::Error) -> Self {
|
||||
Self::ClientError(cle)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for CliError {
|
||||
fn from(ioe: std::io::Error) -> Self {
|
||||
Self::IoError(ioe)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ArgsErr(e) => write!(f, "incorrect arguments. {e}"),
|
||||
Self::ClientError(e) => write!(f, "client error. {e}"),
|
||||
Self::IoError(e) => write!(f, "i/o error. {e}"),
|
||||
Self::QueryError(e) => write!(f, "invalid query. {e}"),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Created on Thu Nov 16 2023
|
||||
*
|
||||
* This file is a part of Skytable
|
||||
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
|
||||
* NoSQL database written by Sayan Nandan ("the Author") with the
|
||||
* vision to provide flexibility in data modelling without compromising
|
||||
* on performance, queryability or scalability.
|
||||
*
|
||||
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use {
|
||||
crate::error::{CliError, CliResult},
|
||||
skytable::{
|
||||
error::ClientResult, query::SQParam, response::Response, Connection, ConnectionTls, Query,
|
||||
},
|
||||
};
|
||||
|
||||
pub trait IsConnection {
|
||||
fn execute_query(&mut self, q: Query) -> ClientResult<Response>;
|
||||
}
|
||||
|
||||
impl IsConnection for Connection {
|
||||
fn execute_query(&mut self, q: Query) -> ClientResult<Response> {
|
||||
self.query(&q)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsConnection for ConnectionTls {
|
||||
fn execute_query(&mut self, q: Query) -> ClientResult<Response> {
|
||||
self.query(&q)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Item {
|
||||
UInt(u64),
|
||||
SInt(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
Bin(Vec<u8>),
|
||||
}
|
||||
|
||||
impl SQParam for Item {
|
||||
fn push(self, buf: &mut Vec<u8>) {
|
||||
match self {
|
||||
Item::UInt(u) => u.push(buf),
|
||||
Item::SInt(s) => s.push(buf),
|
||||
Item::Float(f) => f.push(buf),
|
||||
Item::String(s) => s.push(buf),
|
||||
Item::Bin(b) => SQParam::push(&*b, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Parameterizer {
|
||||
buf: Vec<u8>,
|
||||
i: usize,
|
||||
params: Vec<Item>,
|
||||
query: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Parameterizer {
|
||||
pub fn new(q: String) -> Self {
|
||||
Self {
|
||||
buf: q.into_bytes(),
|
||||
i: 0,
|
||||
params: vec![],
|
||||
query: vec![],
|
||||
}
|
||||
}
|
||||
pub fn parameterize(mut self) -> CliResult<Query> {
|
||||
while self.not_eof() {
|
||||
match self.buf[self.i] {
|
||||
b if b.is_ascii_alphabetic() || b == b'_' => self.read_ident(),
|
||||
b if b.is_ascii_digit() => self.read_unsigned_integer(),
|
||||
b'-' => self.read_signed_integer(),
|
||||
quote_style @ (b'"' | b'\'') => {
|
||||
self.i += 1;
|
||||
self.read_string(quote_style)
|
||||
}
|
||||
b'`' => {
|
||||
self.i += 1;
|
||||
self.read_binary()
|
||||
}
|
||||
sym => {
|
||||
self.i += 1;
|
||||
self.query.push(sym);
|
||||
Ok(())
|
||||
}
|
||||
}?
|
||||
}
|
||||
match String::from_utf8(self.query) {
|
||||
Ok(qstr) => {
|
||||
let mut q = Query::new(&qstr);
|
||||
self.params.into_iter().for_each(|p| {
|
||||
q.push_param(p);
|
||||
});
|
||||
Ok(q)
|
||||
}
|
||||
Err(_) => Err(CliError::QueryError("query is not valid UTF-8".into())),
|
||||
}
|
||||
}
|
||||
fn read_string(&mut self, quote_style: u8) -> CliResult<()> {
|
||||
self.query.push(b'?');
|
||||
let mut string = Vec::new();
|
||||
let mut terminated = false;
|
||||
while self.not_eof() && !terminated {
|
||||
let b = self.buf[self.i];
|
||||
if b == b'\\' {
|
||||
self.i += 1;
|
||||
// escape sequence
|
||||
if self.i == self.buf.len() {
|
||||
// string was not terminated
|
||||
return Err(CliError::QueryError("string not terminated".into()));
|
||||
}
|
||||
match self.buf[self.i] {
|
||||
b'\\' => {
|
||||
// escaped \
|
||||
string.push(b'\\');
|
||||
}
|
||||
b if b == quote_style => {
|
||||
// escape quote
|
||||
string.push(quote_style);
|
||||
}
|
||||
_ => return Err(CliError::QueryError("unknown escape sequence".into())),
|
||||
}
|
||||
}
|
||||
if b == quote_style {
|
||||
terminated = true;
|
||||
} else {
|
||||
string.push(b);
|
||||
}
|
||||
self.i += 1;
|
||||
}
|
||||
if terminated {
|
||||
match String::from_utf8(string) {
|
||||
Ok(s) => self.params.push(Item::String(s)),
|
||||
Err(_) => return Err(CliError::QueryError("invalid UTF-8 string".into())),
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(CliError::QueryError("string not terminated".into()));
|
||||
}
|
||||
}
|
||||
fn read_ident(&mut self) -> CliResult<()> {
|
||||
// we're looking at an ident
|
||||
let start = self.i;
|
||||
self.i += 1;
|
||||
while self.not_eof() {
|
||||
if self.buf[self.i].is_ascii_alphanumeric() || self.buf[self.i] == b'_' {
|
||||
self.i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let stop = self.i;
|
||||
self.query.extend(&self.buf[start..stop]);
|
||||
Ok(())
|
||||
}
|
||||
fn read_float(&mut self, start: usize) -> CliResult<()> {
|
||||
self.read_until_number_escape();
|
||||
let stop = self.i;
|
||||
match core::str::from_utf8(&self.buf[start..stop]).map(|v| v.parse()) {
|
||||
Ok(Ok(num)) => self.params.push(Item::Float(num)),
|
||||
_ => {
|
||||
return Err(CliError::QueryError(
|
||||
"invalid floating point literal".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn read_signed_integer(&mut self) -> CliResult<()> {
|
||||
self.query.push(b'?');
|
||||
// we must have encountered a `-`
|
||||
let start = self.i;
|
||||
self.read_until_number_escape();
|
||||
let stop = self.i;
|
||||
match core::str::from_utf8(&self.buf[start..stop]).map(|v| v.parse()) {
|
||||
Ok(Ok(s)) => self.params.push(Item::SInt(s)),
|
||||
_ => {
|
||||
return Err(CliError::QueryError(
|
||||
"invalid signed integer literal".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn read_unsigned_integer(&mut self) -> CliResult<()> {
|
||||
self.query.push(b'?');
|
||||
let start = self.i;
|
||||
let mut ret = 0u64;
|
||||
while self.not_eof() {
|
||||
match self.buf[self.i] {
|
||||
b if b.is_ascii_digit() => {
|
||||
self.i += 1;
|
||||
ret = match ret
|
||||
.checked_mul(10)
|
||||
.map(|v| v.checked_add((b & 0x0f) as u64))
|
||||
{
|
||||
Some(Some(r)) => r,
|
||||
_ => return Err(CliError::QueryError("bad value for integer".into())),
|
||||
};
|
||||
}
|
||||
b'.' => {
|
||||
self.i += 1;
|
||||
// uh oh, that's a float
|
||||
return self.read_float(start);
|
||||
}
|
||||
b if b == b' ' || b == b'\t' || b.is_ascii_punctuation() => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// nothing else is valid here
|
||||
return Err(CliError::QueryError(
|
||||
"invalid unsigned integer literal".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.params.push(Item::UInt(ret));
|
||||
Ok(())
|
||||
}
|
||||
fn read_until_number_escape(&mut self) {
|
||||
while self.not_eof() {
|
||||
let b = self.buf[self.i];
|
||||
if b == b'\n' || b == b'\t' || b.is_ascii_punctuation() {
|
||||
break;
|
||||
}
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
fn read_binary(&mut self) -> CliResult<()> {
|
||||
self.query.push(b'?');
|
||||
let start = self.i;
|
||||
while self.not_eof() {
|
||||
let b = self.buf[self.i];
|
||||
self.i += 1;
|
||||
if b == b'`' {
|
||||
self.params
|
||||
.push(Item::Bin(self.buf[start..self.i].to_vec()));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(CliError::QueryError("binary literal not terminated".into()))
|
||||
}
|
||||
fn not_eof(&self) -> bool {
|
||||
self.i < self.buf.len()
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Created on Thu Nov 16 2023
|
||||
*
|
||||
* This file is a part of Skytable
|
||||
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
|
||||
* NoSQL database written by Sayan Nandan ("the Author") with the
|
||||
* vision to provide flexibility in data modelling without compromising
|
||||
* on performance, queryability or scalability.
|
||||
*
|
||||
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use {
|
||||
crate::{
|
||||
args::{ClientConfig, ClientConfigKind},
|
||||
error::{CliError, CliResult},
|
||||
query::{self, IsConnection},
|
||||
resp,
|
||||
},
|
||||
crossterm::{cursor, execute, terminal},
|
||||
rustyline::{config::Configurer, error::ReadlineError, DefaultEditor},
|
||||
skytable::Config,
|
||||
std::io::{stdout, ErrorKind},
|
||||
};
|
||||
|
||||
const SKYSH_HISTORY_FILE: &str = ".sky_history";
|
||||
const TXT_WELCOME: &str = include_str!("../help_text/welcome");
|
||||
|
||||
pub fn start(cfg: ClientConfig) -> CliResult<()> {
|
||||
match cfg.kind {
|
||||
ClientConfigKind::Tcp(host, port) => {
|
||||
let c = Config::new(&host, port, &cfg.username, &cfg.password).connect()?;
|
||||
println!(
|
||||
"Authenticated as '{}' on {}:{} over Skyhash/TCP\n---",
|
||||
&cfg.username, &host, &port
|
||||
);
|
||||
repl(c)
|
||||
}
|
||||
ClientConfigKind::Tls(host, port, cert) => {
|
||||
let c = Config::new(&host, port, &cfg.username, &cfg.password).connect_tls(&cert)?;
|
||||
println!(
|
||||
"Authenticated as '{}' on {}:{} over Skyhash/TLS\n---",
|
||||
&cfg.username, &host, &port
|
||||
);
|
||||
repl(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn repl<C: IsConnection>(mut con: C) -> CliResult<()> {
|
||||
let init_editor = || {
|
||||
let mut editor = DefaultEditor::new()?;
|
||||
editor.set_auto_add_history(true);
|
||||
editor.set_history_ignore_dups(true)?;
|
||||
editor.bind_sequence(
|
||||
rustyline::KeyEvent(
|
||||
rustyline::KeyCode::BracketedPasteStart,
|
||||
rustyline::Modifiers::NONE,
|
||||
),
|
||||
rustyline::Cmd::Noop,
|
||||
);
|
||||
match editor.load_history(SKYSH_HISTORY_FILE) {
|
||||
Ok(()) => {}
|
||||
Err(e) => match e {
|
||||
ReadlineError::Io(ref ioe) => match ioe.kind() {
|
||||
ErrorKind::NotFound => {
|
||||
println!("{TXT_WELCOME}");
|
||||
}
|
||||
_ => return Err(e),
|
||||
},
|
||||
e => return Err(e),
|
||||
},
|
||||
}
|
||||
rustyline::Result::Ok(editor)
|
||||
};
|
||||
let mut editor = match init_editor() {
|
||||
Ok(e) => e,
|
||||
Err(e) => fatal!("error: failed to init REPL. {e}"),
|
||||
};
|
||||
loop {
|
||||
match editor.readline("> ") {
|
||||
Ok(line) => match line.as_str() {
|
||||
"!help" => println!("{TXT_WELCOME}"),
|
||||
"exit" => break,
|
||||
"clear" => clear_screen()?,
|
||||
_ => {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match query::Parameterizer::new(line).parameterize() {
|
||||
Ok(q) => resp::format_response(con.execute_query(q)?)?,
|
||||
Err(e) => match e {
|
||||
CliError::QueryError(e) => {
|
||||
eprintln!("[skysh error]: bad query. {e}");
|
||||
continue;
|
||||
}
|
||||
_ => return Err(e),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
Err(e) => match e {
|
||||
ReadlineError::Interrupted | ReadlineError::Eof => {
|
||||
// done
|
||||
break;
|
||||
}
|
||||
ReadlineError::WindowResized => {}
|
||||
e => fatal!("error: failed to read line REPL. {e}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
editor
|
||||
.save_history(SKYSH_HISTORY_FILE)
|
||||
.expect("failed to save history");
|
||||
println!("Goodbye!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_screen() -> std::io::Result<()> {
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
|
||||
execute!(stdout, cursor::MoveTo(0, 0))
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Created on Thu Nov 16 2023
|
||||
*
|
||||
* This file is a part of Skytable
|
||||
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
|
||||
* NoSQL database written by Sayan Nandan ("the Author") with the
|
||||
* vision to provide flexibility in data modelling without compromising
|
||||
* on performance, queryability or scalability.
|
||||
*
|
||||
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use {
|
||||
crate::error::CliResult,
|
||||
crossterm::{
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
ExecutableCommand,
|
||||
},
|
||||
skytable::response::{Response, Row, Value},
|
||||
std::io::{self, Write},
|
||||
};
|
||||
|
||||
pub fn format_response(resp: Response) -> CliResult<()> {
|
||||
match resp {
|
||||
Response::Empty => print_cyan("(Okay)\n")?,
|
||||
Response::Error(e) => print_red(&format!("(server error code: {e})\n"))?,
|
||||
Response::Value(v) => {
|
||||
print_value(v)?;
|
||||
println!();
|
||||
}
|
||||
Response::Row(r) => {
|
||||
print_row(r)?;
|
||||
println!();
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_row(r: Row) -> CliResult<()> {
|
||||
print!("(");
|
||||
let mut columns = r.into_values().into_iter().peekable();
|
||||
while let Some(cell) = columns.next() {
|
||||
print_value(cell)?;
|
||||
if columns.peek().is_some() {
|
||||
print!(", ");
|
||||
}
|
||||
}
|
||||
print!(")");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_value(v: Value) -> CliResult<()> {
|
||||
match v {
|
||||
Value::Null => print_gray("null")?,
|
||||
Value::String(s) => print_string(&s),
|
||||
Value::Binary(b) => print_binary(&b),
|
||||
Value::Bool(b) => print!("{b}"),
|
||||
Value::UInt8(i) => print!("{i}"),
|
||||
Value::UInt16(i) => print!("{i}"),
|
||||
Value::UInt32(i) => print!("{i}"),
|
||||
Value::UInt64(i) => print!("{i}"),
|
||||
Value::SInt8(i) => print!("{i}"),
|
||||
Value::SInt16(i) => print!("{i}"),
|
||||
Value::SInt32(i) => print!("{i}"),
|
||||
Value::SInt64(i) => print!("{i}"),
|
||||
Value::Float32(f) => print!("{f}"),
|
||||
Value::Float64(f) => print!("{f}"),
|
||||
Value::List(items) => {
|
||||
print!("[");
|
||||
let mut items = items.into_iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
print_value(item)?;
|
||||
if items.peek().is_some() {
|
||||
print!(", ");
|
||||
}
|
||||
}
|
||||
print!("]");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_binary(b: &[u8]) {
|
||||
let mut it = b.into_iter().peekable();
|
||||
print!("[");
|
||||
while let Some(byte) = it.next() {
|
||||
print!("{byte}");
|
||||
if it.peek().is_some() {
|
||||
print!(", ");
|
||||
}
|
||||
}
|
||||
print!("]");
|
||||
}
|
||||
|
||||
fn print_string(s: &str) {
|
||||
print!("\"");
|
||||
for ch in s.chars() {
|
||||
if ch == '"' || ch == '\'' {
|
||||
print!("\\{ch}");
|
||||
} else if ch == '\t' {
|
||||
print!("\\t");
|
||||
} else if ch == '\n' {
|
||||
print!("\\n");
|
||||
} else {
|
||||
print!("{ch}");
|
||||
}
|
||||
}
|
||||
print!("\"");
|
||||
}
|
||||
|
||||
fn print_gray(s: &str) -> std::io::Result<()> {
|
||||
print_colored_text(s, Color::White)
|
||||
}
|
||||
|
||||
fn print_red(s: &str) -> std::io::Result<()> {
|
||||
print_colored_text(s, Color::Red)
|
||||
}
|
||||
|
||||
fn print_cyan(s: &str) -> std::io::Result<()> {
|
||||
print_colored_text(s, Color::Cyan)
|
||||
}
|
||||
|
||||
fn print_colored_text(text: &str, color: Color) -> std::io::Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
stdout.execute(SetForegroundColor(color))?;
|
||||
print!("{text}");
|
||||
stdout.flush()?;
|
||||
stdout.execute(ResetColor)?;
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue