start of refactor of repl to use clap

This commit is contained in:
pedrocarlo
2025-03-07 02:51:56 -03:00
parent 4ee60348f2
commit 02c466cb1f
6 changed files with 330 additions and 196 deletions

View File

@@ -1,20 +1,19 @@
use crate::{ use crate::{
commands::{args::EchoMode, import::ImportFile, Command, CommandParser},
helper::LimboHelper, helper::LimboHelper,
import::{ImportFile, IMPORT_HELP}, input::{get_io, get_writer, DbLocation, OutputMode, Settings},
input::{get_io, get_writer, DbLocation, OutputMode, Settings, HELP_MSG},
opcodes_dictionary::OPCODE_DESCRIPTIONS, opcodes_dictionary::OPCODE_DESCRIPTIONS,
}; };
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table}; use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table};
use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult}; use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult};
use clap::{Parser, ValueEnum}; use clap::Parser;
use rustyline::{history::DefaultHistory, Editor}; use rustyline::{history::DefaultHistory, Editor};
use std::{ use std::{
fmt, fmt,
io::{self, Write}, io::{self, Write},
path::PathBuf, path::PathBuf,
rc::Rc, rc::Rc,
str::FromStr,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Arc,
@@ -52,116 +51,6 @@ pub struct Opts {
pub experimental_mvcc: bool, pub experimental_mvcc: bool,
} }
#[derive(Debug, Clone)]
pub enum Command {
/// Exit this program with return-code CODE
Exit,
/// Quit the shell
Quit,
/// Open a database file
Open,
/// Display help message
Help,
/// Display schema for a table
Schema,
/// Set output file (or stdout if empty)
SetOutput,
/// Set output display mode
OutputMode,
/// Show vdbe opcodes
Opcodes,
/// Change the current working directory
Cwd,
/// Display information about settings
ShowInfo,
/// Set the value of NULL to be printed in 'list' mode
NullValue,
/// Toggle 'echo' mode to repeat commands before execution
Echo,
/// Display tables
Tables,
/// Import data from FILE into TABLE
Import,
/// Loads an extension library
LoadExtension,
/// Dump the current database as a list of SQL statements
Dump,
/// List vfs modules available
ListVfs,
}
impl Command {
fn min_args(&self) -> usize {
1 + match self {
Self::Exit
| Self::Quit
| Self::Schema
| Self::Help
| Self::Opcodes
| Self::ShowInfo
| Self::Tables
| Self::SetOutput
| Self::ListVfs
| Self::Dump => 0,
Self::Open
| Self::OutputMode
| Self::Cwd
| Self::Echo
| Self::NullValue
| Self::LoadExtension => 1,
Self::Import => 2,
}
}
fn usage(&self) -> &str {
match self {
Self::Exit => ".exit ?<CODE>",
Self::Quit => ".quit",
Self::Open => ".open <file>",
Self::Help => ".help",
Self::Schema => ".schema ?<table>?",
Self::Opcodes => ".opcodes",
Self::OutputMode => ".mode list|pretty",
Self::SetOutput => ".output ?file?",
Self::Cwd => ".cd <directory>",
Self::ShowInfo => ".show",
Self::NullValue => ".nullvalue <string>",
Self::Echo => ".echo on|off",
Self::Tables => ".tables",
Self::LoadExtension => ".load",
Self::Dump => ".dump",
Self::Import => &IMPORT_HELP,
Self::ListVfs => ".vfslist",
}
}
}
impl FromStr for Command {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
".exit" => Ok(Self::Exit),
".quit" => Ok(Self::Quit),
".open" => Ok(Self::Open),
".help" => Ok(Self::Help),
".schema" => Ok(Self::Schema),
".tables" => Ok(Self::Tables),
".opcodes" => Ok(Self::Opcodes),
".mode" => Ok(Self::OutputMode),
".output" => Ok(Self::SetOutput),
".cd" => Ok(Self::Cwd),
".show" => Ok(Self::ShowInfo),
".nullvalue" => Ok(Self::NullValue),
".echo" => Ok(Self::Echo),
".import" => Ok(Self::Import),
".load" => Ok(Self::LoadExtension),
".dump" => Ok(Self::Dump),
".vfslist" => Ok(Self::ListVfs),
_ => Err("Unknown command".to_string()),
}
}
}
const PROMPT: &str = "limbo> "; const PROMPT: &str = "limbo> ";
pub struct Limbo<'a> { pub struct Limbo<'a> {
@@ -265,7 +154,7 @@ impl<'a> Limbo<'a> {
fn handle_first_input(&mut self, cmd: &str) { fn handle_first_input(&mut self, cmd: &str) {
if cmd.trim().starts_with('.') { if cmd.trim().starts_with('.') {
self.handle_dot_command(cmd); self.handle_dot_command(&cmd[1..]);
} else { } else {
self.run_query(cmd); self.run_query(cmd);
} }
@@ -414,12 +303,16 @@ impl<'a> Limbo<'a> {
self.conn.close() self.conn.close()
} }
fn toggle_echo(&mut self, arg: &str) { fn toggle_echo(&mut self, arg: EchoMode) {
match arg.trim().to_lowercase().as_str() { match arg {
"on" => self.opts.echo = true, EchoMode::On => self.opts.echo = true,
"off" => self.opts.echo = false, EchoMode::Off => self.opts.echo = false,
_ => {}
} }
// match arg.trim().to_lowercase().as_str() {
// "on" => self.opts.echo = true,
// "off" => self.opts.echo = false,
// _ => {}
// }
} }
fn open_db(&mut self, path: &str, vfs_name: Option<&str>) -> anyhow::Result<()> { fn open_db(&mut self, path: &str, vfs_name: Option<&str>) -> anyhow::Result<()> {
@@ -522,7 +415,7 @@ impl<'a> Limbo<'a> {
return Ok(()); return Ok(());
} }
if line.starts_with('.') { if line.starts_with('.') {
self.handle_dot_command(line); self.handle_dot_command(&line[1..]);
let _ = self.reset_line(line); let _ = self.reset_line(line);
return Ok(()); return Ok(());
} }
@@ -578,46 +471,44 @@ impl<'a> Limbo<'a> {
if args.is_empty() { if args.is_empty() {
return; return;
} }
if let Ok(ref cmd) = Command::from_str(args[0]) { // else {
if args.len() < cmd.min_args() { // // let _ = self.write_fmt(format_args!(
let _ = self.write_fmt(format_args!( // // "Unknown command: {}\nenter: .help for all available commands",
"Insufficient arguments: USAGE: {}", // // args[0]
cmd.usage() // // ));
)); match CommandParser::try_parse_from(args) {
return; Err(err) => {
let _ = self.write_fmt(format_args!("{err}"));
} }
match cmd { Ok(cmd) => match cmd.command {
Command::Exit => { Command::Exit(args) => {
let code = args.get(1).and_then(|c| c.parse::<i32>().ok()).unwrap_or(0); // let code = args.get(1).and_then(|c| c.parse::<i32>().ok()).unwrap_or(0);
std::process::exit(code); std::process::exit(args.code);
} }
Command::Quit => { Command::Quit => {
let _ = self.writeln("Exiting Limbo SQL Shell."); let _ = self.writeln("Exiting Limbo SQL Shell.");
let _ = self.close_conn(); let _ = self.close_conn();
std::process::exit(0) std::process::exit(0)
} }
Command::Open => { Command::Open(args) => {
let vfs = args.get(2).map(|s| &**s); if self.open_db(&args.path, args.vfs_name.as_deref()).is_err() {
if self.open_db(args[1], vfs).is_err() {
let _ = self.writeln("Error: Unable to open database file."); let _ = self.writeln("Error: Unable to open database file.");
} }
} }
Command::Schema => { Command::Schema(args) => {
let table_name = args.get(1).copied(); if let Err(e) = self.display_schema(args.table_name.as_deref()) {
if let Err(e) = self.display_schema(table_name) {
let _ = self.writeln(e.to_string()); let _ = self.writeln(e.to_string());
} }
} }
Command::Tables => { Command::Tables(args) => {
let pattern = args.get(1).copied(); if let Err(e) = self.display_tables(args.pattern.as_deref()) {
if let Err(e) = self.display_tables(pattern) {
let _ = self.writeln(e.to_string()); let _ = self.writeln(e.to_string());
} }
} }
Command::Opcodes => { Command::Opcodes(args) => {
if args.len() > 1 { if let Some(opcode) = args.opcode {
for op in &OPCODE_DESCRIPTIONS { for op in &OPCODE_DESCRIPTIONS {
if op.name.eq_ignore_ascii_case(args.get(1).unwrap().trim()) { if op.name.eq_ignore_ascii_case(opcode.trim()) {
let _ = self.write_fmt(format_args!("{}", op)); let _ = self.write_fmt(format_args!("{}", op));
} }
} }
@@ -627,51 +518,56 @@ impl<'a> Limbo<'a> {
} }
} }
} }
Command::NullValue => { Command::NullValue(args) => {
self.opts.null_value = args[1].to_string(); self.opts.null_value = args.value;
} }
Command::OutputMode => match OutputMode::from_str(args[1], true) { Command::OutputMode(args) => {
Ok(mode) => { if let Err(e) = self.set_mode(args.mode) {
if let Err(e) = self.set_mode(mode) { let _ = self.write_fmt(format_args!("{}", e));
let _ = self.write_fmt(format_args!("Error: {}", e));
}
} }
Err(e) => { }
let _ = self.writeln(e); // OutputMode::from_str(args[1], true) {
} // Ok(mode) => {
}, // if let Err(e) = self.set_mode(mode) {
Command::SetOutput => { // let _ = self.write_fmt(format_args!("Error: {}", e));
if args.len() == 2 { // }
if let Err(e) = self.set_output_file(args[1]) { // }
// Err(e) => {
// let _ = self.writeln(e);
// }
// },
Command::SetOutput(args) => {
if let Some(path) = args.path {
if let Err(e) = self.set_output_file(&path) {
let _ = self.write_fmt(format_args!("Error: {}", e)); let _ = self.write_fmt(format_args!("Error: {}", e));
} }
} else { } else {
self.set_output_stdout(); self.set_output_stdout();
} }
} }
Command::Echo => { Command::Echo(args) => {
self.toggle_echo(args[1]); self.toggle_echo(args.mode);
} }
Command::Cwd => { Command::Cwd(args) => {
let _ = std::env::set_current_dir(args[1]); let _ = std::env::set_current_dir(args.directory);
} }
Command::ShowInfo => { Command::ShowInfo => {
let _ = self.show_info(); let _ = self.show_info();
} }
Command::Help => { // Command::Help => {
let _ = self.writeln(HELP_MSG); // let _ = self.writeln(HELP_MSG);
} // }
Command::Import => { Command::Import(args) => {
let mut import_file = let mut import_file =
ImportFile::new(self.conn.clone(), self.io.clone(), &mut self.writer); ImportFile::new(self.conn.clone(), self.io.clone(), &mut self.writer);
if let Err(e) = import_file.import(&args) { import_file.import(args)
let _ = self.writeln(e.to_string()); // if let Err(e) = import_file.import(args) {
}; // let _ = self.writeln(e.to_string());
// };
} }
Command::LoadExtension => Command::LoadExtension(args) => {
{
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
if let Err(e) = self.handle_load_extension(args[1]) { if let Err(e) = self.handle_load_extension(&args.path) {
let _ = self.writeln(&e); let _ = self.writeln(&e);
} }
} }
@@ -686,12 +582,7 @@ impl<'a> Limbo<'a> {
let _ = self.writeln(v); let _ = self.writeln(v);
}); });
} }
} },
} else {
let _ = self.write_fmt(format_args!(
"Unknown command: {}\nenter: .help for all available commands",
args[0]
));
} }
} }

82
cli/commands/args.rs Normal file
View File

@@ -0,0 +1,82 @@
use clap::{Args, ValueEnum};
use crate::input::OutputMode;
#[derive(Debug, Clone, Args)]
pub struct ExitArgs {
/// Exit code
#[arg(default_value_t = 0)]
pub code: i32,
}
#[derive(Debug, Clone, Args)]
pub struct OpenArgs {
/// Path to open database
pub path: String,
/// Name of VFS
pub vfs_name: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct SchemaArgs {
/// Table name to visualize schema
pub table_name: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct SetOutputArgs {
/// File path to send output to
pub path: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct OutputModeArgs {
#[arg(value_enum)]
pub mode: OutputMode,
}
#[derive(Debug, Clone, Args)]
pub struct OpcodesArgs {
/// Opcode to display description
pub opcode: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct CwdArgs {
/// Target directory
pub directory: String,
}
#[derive(Debug, Clone, Args)]
pub struct NullValueArgs {
pub value: String,
}
#[derive(Debug, Clone, Args)]
pub struct EchoArgs {
#[arg(value_enum)]
pub mode: EchoMode,
}
#[derive(Debug, ValueEnum, Clone)]
pub enum EchoMode {
On,
Off,
}
#[derive(Debug, Clone, Args)]
pub struct TablesArgs {
pub pattern: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct LoadExtensionArgs {
/// Path to extension file
pub path: String,
}
#[derive(Debug, Clone, Args)]
pub struct ListVfsArgs {
/// Path to extension file
pub path: String,
}

View File

@@ -1,5 +1,5 @@
use anyhow::Error; use anyhow::Error;
use clap::Parser; use clap::{Parser, Subcommand, Args};
use limbo_core::Connection; use limbo_core::Connection;
use std::{ use std::{
fs::File, fs::File,
@@ -9,14 +9,13 @@ use std::{
sync::{Arc, LazyLock}, sync::{Arc, LazyLock},
}; };
pub static IMPORT_HELP: LazyLock<String> = LazyLock::new(|| { // pub static IMPORT_HELP: LazyLock<String> = LazyLock::new(|| {
let empty: [&'static str; 2] = [".import", "--help"]; // let empty: [&'static str; 2] = [".import", "--help"];
let opts = ImportArgs::try_parse_from(empty); // let opts = ImportArgs::try_parse_from(empty);
opts.map_err(|e| e.to_string()).unwrap_err() // opts.map_err(|e| e.to_string()).unwrap_err()
}); // });
#[derive(Debug, Parser)] #[derive(Debug, Clone, Args)]
#[command(name = ".import")]
pub struct ImportArgs { pub struct ImportArgs {
/// Use , and \n as column and row separators /// Use , and \n as column and row separators
#[arg(long, default_value = "true")] #[arg(long, default_value = "true")]
@@ -46,15 +45,16 @@ impl<'a> ImportFile<'a> {
Self { conn, io, writer } Self { conn, io, writer }
} }
pub fn import(&mut self, args: &[&str]) -> Result<(), Error> { pub fn import(&mut self, args: ImportArgs) {
let import_args = ImportArgs::try_parse_from(args.iter()); self.import_csv(args);
match import_args { // let import_args = ImportArgs::try_parse_from(args.iter());
Ok(args) => { // match import_args {
self.import_csv(args); // Ok(args) => {
Ok(()) // self.import_csv(args);
} // Ok(())
Err(err) => Err(anyhow::anyhow!(err.to_string())), // }
} // Err(err) => Err(anyhow::anyhow!(err.to_string())),
// }
} }
pub fn import_csv(&mut self, args: ImportArgs) { pub fn import_csv(&mut self, args: ImportArgs) {

151
cli/commands/mod.rs Normal file
View File

@@ -0,0 +1,151 @@
pub mod args;
pub mod import;
use args::{
CwdArgs, EchoArgs, ExitArgs, LoadExtensionArgs, NullValueArgs, OpcodesArgs, OpenArgs,
OutputModeArgs, SchemaArgs, SetOutputArgs, TablesArgs,
};
use clap::Parser;
use import::ImportArgs;
use crate::input::{AFTER_HELP_MSG, BEFORE_HELP_MSG};
#[derive(Parser, Debug)]
#[command(
multicall = true,
arg_required_else_help(false),
before_help(BEFORE_HELP_MSG),
after_help(AFTER_HELP_MSG)
)]
pub struct CommandParser {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, clap::Subcommand)]
#[command(disable_help_flag(false), disable_version_flag(true))]
pub enum Command {
/// Exit this program with return-code CODE
#[command(subcommand_value_name = ".exit")]
Exit(ExitArgs),
/// Quit the shell
#[command(subcommand_value_name = ".quit")]
Quit,
/// Open a database file
#[command(subcommand_value_name = ".open")]
Open(OpenArgs),
// Display help message
// Help,
/// Display schema for a table
#[command(subcommand_value_name = ".schema")]
Schema(SchemaArgs),
/// Set output file (or stdout if empty)
#[command(name = "output", subcommand_value_name = ".output")]
SetOutput(SetOutputArgs),
/// Set output display mode
#[command(
name = "mode",
subcommand_value_name = ".mode",
arg_required_else_help(false)
)]
OutputMode(OutputModeArgs),
/// Show vdbe opcodes
#[command(subcommand_value_name = ".opcodes")]
Opcodes(OpcodesArgs),
/// Change the current working directory
#[command(name = "cd", subcommand_value_name = ".cd")]
Cwd(CwdArgs),
/// Display information about settings
#[command(name = "show")]
ShowInfo,
/// Set the value of NULL to be printed in 'list' mode
#[command(name = "nullvalue", subcommand_value_name = ".nullvalue")]
NullValue(NullValueArgs),
/// Toggle 'echo' mode to repeat commands before execution
#[command(subcommand_value_name = ".echo")]
Echo(EchoArgs),
/// Display tables
#[command(subcommand_value_name = ".tables")]
Tables(TablesArgs),
/// Import data from FILE into TABLE
#[command(subcommand_value_name = ".import")]
Import(ImportArgs),
/// Loads an extension library
#[command(name = "load", subcommand_value_name = ".load")]
LoadExtension(LoadExtensionArgs),
/// Dump the current database as a list of SQL statements
#[command(subcommand_value_name = ".dump")]
Dump,
/// List vfs modules available
#[command(name = "listvfs", subcommand_value_name = ".dump")]
ListVfs,
}
// impl Command {
// pub fn min_args(&self) -> usize {
// 1 + match self {
// Self::Exit
// | Self::Quit
// | Self::Schema
// | Self::Help
// | Self::Opcodes
// | Self::ShowInfo
// | Self::Tables
// | Self::SetOutput
// | Self::Dump => 0,
// Self::Open
// | Self::OutputMode
// | Self::Cwd
// | Self::Echo
// | Self::NullValue
// | Self::LoadExtension => 1,
// Self::Import => 2,
// }
// }
// pub fn usage(&self) -> &str {
// match self {
// Self::Exit => ".exit ?<CODE>",
// Self::Quit => ".quit",
// Self::Open => ".open <file>",
// Self::Help => ".help",
// Self::Schema => ".schema ?<table>?",
// Self::Opcodes => ".opcodes",
// Self::OutputMode => ".mode list|pretty",
// Self::SetOutput => ".output ?file?",
// Self::Cwd => ".cd <directory>",
// Self::ShowInfo => ".show",
// Self::NullValue => ".nullvalue <string>",
// Self::Echo => ".echo on|off",
// Self::Tables => ".tables",
// Self::LoadExtension => ".load",
// Self::Dump => ".dump",
// Self::Import => &IMPORT_HELP,
// }
// }
// }
// impl FromStr for Command {
// type Err = String;
// fn from_str(s: &str) -> Result<Self, Self::Err> {
// match s {
// ".exit" => Ok(Self::Exit),
// ".quit" => Ok(Self::Quit),
// ".open" => Ok(Self::Open),
// ".help" => Ok(Self::Help),
// ".schema" => Ok(Self::Schema),
// ".tables" => Ok(Self::Tables),
// ".opcodes" => Ok(Self::Opcodes),
// ".mode" => Ok(Self::OutputMode),
// ".output" => Ok(Self::SetOutput),
// ".cd" => Ok(Self::Cwd),
// ".show" => Ok(Self::ShowInfo),
// ".nullvalue" => Ok(Self::NullValue),
// ".echo" => Ok(Self::Echo),
// ".import" => Ok(Self::Import),
// ".load" => Ok(Self::LoadExtension),
// ".dump" => Ok(Self::Dump),
// _ => Err("Unknown command".to_string()),
// }
// }
// }

View File

@@ -232,3 +232,13 @@ Usage Examples:
Note: Note:
- All SQL commands must end with a semicolon (;). - All SQL commands must end with a semicolon (;).
- Special commands do not require a semicolon."#; - Special commands do not require a semicolon."#;
pub const BEFORE_HELP_MSG: &str = r#"
Limbo SQL Shell Help
==============
Welcome to the Limbo SQL Shell! You can execute any standard SQL command here.
In addition to standard SQL commands, the following special commands are available:"#;
pub const AFTER_HELP_MSG: &str = r#"Note:
- All SQL commands must end with a semicolon (;).
- Special commands start with a dot and do not require a semicolon."#;

View File

@@ -1,9 +1,9 @@
#![allow(clippy::arc_with_non_send_sync)] #![allow(clippy::arc_with_non_send_sync)]
mod app; mod app;
mod helper; mod helper;
mod import;
mod input; mod input;
mod opcodes_dictionary; mod opcodes_dictionary;
mod commands;
use rustyline::{error::ReadlineError, Config, Editor}; use rustyline::{error::ReadlineError, Config, Editor};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;