summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Wernick <justin@worthe-it.co.za>2023-02-27 23:12:32 +0200
committerJustin Wernick <justin@worthe-it.co.za>2023-02-27 23:12:32 +0200
commit9f0f47abaa934b66da5b302236bfc89f95a7f329 (patch)
tree37b20fa001c42e61da54ada3a905fda6463fe050
parent5ac9161b7edabbb95c1f8a5d68af62a567687190 (diff)
Revamp parsing to support more complex commands
-rw-r--r--Cargo.lock18
-rw-r--r--Cargo.toml2
-rw-r--r--readme.org1
-rw-r--r--src/main.rs37
-rw-r--r--src/parser.rs61
-rw-r--r--tests/cli.rs19
6 files changed, 123 insertions, 15 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e609fb7..332101e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -134,6 +134,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -148,6 +154,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 <argument>
- [ ] 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<dyn Error>> {
+mod parser;
+
+use parser::Command;
+
+fn prompt() -> Result<(), ShackleError> {
print!("> ");
io::stdout().flush()?;
Ok(())
}
-fn read_stdin() -> Result<String, Box<dyn Error>> {
+fn read_stdin() -> Result<String, ShackleError> {
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
Ok(buffer)
}
-fn main() -> Result<(), Box<dyn Error>> {
+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::<Command>() {
+ 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<Self, Self::Err> {
+ 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(())
+}