#![allow(dead_code)]
#![allow(non_upper_case_globals)]
#![allow(unused_must_use)]
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::time::Instant;
use regex::Regex;
use clap::{Arg, ArgAction, Command};
use rustyline::DefaultEditor;
use directories::ProjectDirs;
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Config: u32 {
const VERBOSE_ERRORS = 0b00000001;
const CAPTURE_GROUPS = 0b00000010;
const COMPILE_TIME = 0b00000100;
}
}
impl Default for Config {
fn default() -> Config {
Config::VERBOSE_ERRORS | Config::COMPILE_TIME
}
}
const HELP: &str = "\
:t - Toggle compile time display
:g - Toggle capture groups display
:v - Toggle verbose errors
:h - Print this menu
:q - Quit";
const MENU_PRMT: &str = ":b - Go back to the regex prompt";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Action {
Continue,
Loop,
ToRegexPrompt,
Exit,
}
fn options_menu(line: &str, config: &mut Config) -> Action {
let mut stderr = io::stderr();
match line {
":q" => Action::Exit,
":v" => {
config.toggle(Config::VERBOSE_ERRORS);
if config.contains(Config::VERBOSE_ERRORS) {
write!(stderr, "Verbose errors: on\n");
} else {
write!(stderr, "Verbose errors: off\n");
}
Action::Loop
}
":t" => {
config.toggle(Config::COMPILE_TIME);
if config.contains(Config::COMPILE_TIME) {
write!(stderr, "Show compile time: on\n");
} else {
write!(stderr, "Show compile time: off\n");
}
Action::Loop
}
":b" => Action::ToRegexPrompt,
":g" => {
config.toggle(Config::CAPTURE_GROUPS);
if config.contains(Config::CAPTURE_GROUPS) {
write!(stderr, "Show capture groups: on\n");
} else {
write!(stderr, "Show capture groups: off\n");
}
Action::Loop
}
":h" | ":?" => {
write!(stderr, "{}\n", HELP);
Action::Loop
}
_ => Action::Continue,
}
}
fn regex_prompt(editor: &mut DefaultEditor, config: &mut Config) -> bool {
let mut stderr = io::stderr();
let line = editor.readline("Input> ").expect("Failed to read line!");
editor.add_history_entry(line.as_str());
match options_menu(&line, config) {
Action::Continue => {}
Action::ToRegexPrompt | Action::Loop => return true,
Action::Exit => return false,
}
let t1 = Instant::now();
let reg = match Regex::new(&line) {
Ok(r) => r,
Err(e) => {
if config.contains(Config::VERBOSE_ERRORS) {
write!(stderr, "Error compiling regex: {:?}\n", e);
} else {
stderr.write(b"Failed to compile regex\n");
stderr.write(b"Turn on verbose errors with :v\n");
}
return true;
}
};
if config.contains(Config::COMPILE_TIME) {
write!(stderr, "Regex compiled in {}ns\n", t1.elapsed().as_nanos());
}
prompt(editor, ®, config)
}
fn prompt(editor: &mut DefaultEditor, reg: &Regex, config: &mut Config) -> bool {
let mut stderr = io::stderr();
let prompt_str = format!("Regex({})> ", reg.as_str());
loop {
let line = editor.readline(&prompt_str).expect("Failed to read line");
editor.add_history_entry(line.as_str());
match options_menu(&line, config) {
Action::Exit => return false,
Action::Loop => continue,
Action::ToRegexPrompt => return true,
Action::Continue => {
if config.contains(Config::CAPTURE_GROUPS) {
let caps = reg.captures_iter(&line).enumerate();
write!(stderr, "Captures:\n");
for (i, outer_cap) in caps {
for (j, cap) in outer_cap.iter().enumerate() {
write!(stderr,
"{}:{}: {}\n",
i,
j,
if let Some(c) = cap { c.as_str() } else { "None" });
}
}
} else if reg.is_match(&line) {
write!(stderr, "Matched\n");
} else {
write!(stderr, "Failed to match\n");
}
}
}
}
}
fn with_history_file<F>(mut f: F)
where
F: FnMut(&PathBuf),
{
let dirs = match ProjectDirs::from("", "Lucas Salibian", "regtest") {
Some(d) => d,
None => {
println!("Failed to determine history file location");
return;
}
};
let data_dir = dirs.data_dir();
if let Err(e) = std::fs::create_dir_all(data_dir) {
println!("Failed to create data directory: {:?}", e);
return;
}
let mut path = data_dir.to_path_buf();
path.push("history");
f(&path);
}
fn main() {
let mut config = Config::default();
let matches = Command::new("regtest")
.version(env!("CARGO_PKG_VERSION"))
.author("Lucas Salibian <lucas.salibian@gmail.com>")
.about("Test regexes from the command line")
.arg(Arg::new("no-verbose-errors")
.long("no-verbose-errors")
.help("Disable verbose errors when the regex fails to compile")
.action(ArgAction::SetTrue))
.arg(Arg::new("capture")
.short('c')
.long("capture")
.help("Enable capture group display after matching test")
.action(ArgAction::SetTrue))
.arg(Arg::new("no-compile-time")
.long("no-compile-time")
.help("Disable showing the amount of time it took to compile the regular expression.")
.action(ArgAction::SetTrue))
.get_matches();
if matches.get_flag("no-verbose-errors") {
config.remove(Config::VERBOSE_ERRORS);
}
if matches.get_flag("capture") {
config.insert(Config::CAPTURE_GROUPS);
}
let mut editor = DefaultEditor::new().unwrap();
with_history_file(|path| { editor.load_history(path); });
loop {
if !regex_prompt(&mut editor, &mut config) {
break;
}
}
with_history_file(|path| { editor.save_history(path).unwrap(); });
}