From 9f0f47abaa934b66da5b302236bfc89f95a7f329 Mon Sep 17 00:00:00 2001 From: Justin Wernick Date: Mon, 27 Feb 2023 23:12:32 +0200 Subject: Revamp parsing to support more complex commands --- Cargo.lock | 18 ++++++++++++++++++ Cargo.toml | 2 ++ readme.org | 1 + src/main.rs | 37 +++++++++++++++++++++--------------- src/parser.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/cli.rs | 19 +++++++++++++++++++ 6 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 src/parser.rs diff --git a/Cargo.lock b/Cargo.lock index e609fb7..332101e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "nix" version = "0.25.1" @@ -147,6 +153,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -270,7 +286,9 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", + "nom", "rexpect", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 48e50e9..26bdb76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +nom = "7.1.3" rexpect = "0.5.0" +thiserror = "1.0.38" [dev-dependencies] anyhow = "1.0.69" diff --git a/readme.org b/readme.org index 99f4e33..d42f8d5 100644 --- a/readme.org +++ b/readme.org @@ -16,6 +16,7 @@ Pijul. - [X] interactive command prompt - [X] exit command - [ ] git init of private repo +- [ ] responds to unknown commands - [ ] git fetch - git receive-pack - [ ] git push diff --git a/src/main.rs b/src/main.rs index 93880de..0df2754 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,45 @@ -use std::error::Error; use std::{io, io::Write}; +use thiserror::Error; -fn prompt() -> Result<(), Box> { +mod parser; + +use parser::Command; + +fn prompt() -> Result<(), ShackleError> { print!("> "); io::stdout().flush()?; Ok(()) } -fn read_stdin() -> Result> { +fn read_stdin() -> Result { let mut buffer = String::new(); io::stdin().read_line(&mut buffer)?; Ok(buffer) } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ShackleError> { loop { prompt()?; let user_input = read_stdin()?; - if user_input.len() == 0 { - // control-d or end of input. Needs to be specially handled before - // the match because this is identical to whitespace after the trim. - break; - } - - match user_input.trim() { - "" => {} - "exit" => { + match user_input.parse::() { + Err(unknown_input) => { + println!("Unknown input \"{}\"", unknown_input); + } + Ok(Command::Whitespace) => {} + Ok(Command::Exit) => { break; } - other_input => { - println!("Unknown input {}", other_input); + Ok(Command::GitInit(repo_name)) => { + println!("Successfully created {}.git", repo_name); } } } Ok(()) } + +#[derive(Error, Debug)] +enum ShackleError { + #[error(transparent)] + IoError(#[from] io::Error), +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..2f65180 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,61 @@ +use nom::{ + branch::alt, + bytes::complete::{is_not, tag}, + character::complete::{multispace0, multispace1}, + combinator::{eof, map, value}, + error::ParseError, + sequence::delimited, + sequence::tuple, + Finish, IResult, +}; +use std::str::FromStr; + +#[derive(Clone)] +pub enum Command { + Whitespace, + Exit, + GitInit(String), +} + +impl FromStr for Command { + // the error must be owned as well + type Err = String; + + fn from_str(s: &str) -> Result { + match command_parser(s).finish() { + Ok((remaining, command)) => { + if remaining.trim().is_empty() { + Ok(command) + } else { + Err(s.trim().to_owned()) + } + } + Err(_) => Err(s.trim().to_owned()), + } + } +} + +fn command_parser(input: &str) -> IResult<&str, Command> { + alt(( + value(Command::Exit, eof), + value(Command::Whitespace, tuple((multispace1, eof))), + value(Command::Exit, ws(tag("exit"))), + map( + ws(tuple((tag("git-init"), multispace1, not_space))), + |(_, _, repo_name)| Command::GitInit(repo_name.to_owned()), + ), + ))(input) +} + +/// A combinator that takes a parser `inner` and produces a parser that also consumes both leading and +/// trailing whitespace, returning the output of `inner`. +fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> +where + F: FnMut(&'a str) -> IResult<&'a str, O, E>, +{ + delimited(multispace0, inner, multispace0) +} + +fn not_space(s: &str) -> IResult<&str, &str> { + is_not(" \t\r\n")(s) +} diff --git a/tests/cli.rs b/tests/cli.rs index 8197a92..672a798 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -45,3 +45,22 @@ fn quits_when_exit_command_is_sent() -> Result<()> { p.exp_eof()?; Ok(()) } + +#[test] +fn reports_error_with_nonsense_input() -> Result<()> { + let mut p = spawn_interactive_process()?; + p.send_line("asdfg")?; + p.exp_string("Unknown input \"asdfg\"")?; + expect_prompt(&mut p)?; + Ok(()) +} + +#[test] +fn can_init_a_new_git_repo() -> Result<()> { + let mut p = spawn_interactive_process()?; + p.send_line("git-init my-new-repo")?; + p.exp_string("Successfully created my-new-repo.git")?; + expect_prompt(&mut p)?; + // TODO: assert that the repo is actually there? + Ok(()) +} -- cgit v1.2.3