From f5a00e9090b9d81936137c3fc676cfd0ad25430c Mon Sep 17 00:00:00 2001 From: Justin Wernick Date: Fri, 17 Mar 2023 11:10:53 +0200 Subject: Use shlex to split shell arguments --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + readme.org | 2 +- src/main.rs | 4 ++-- src/parser.rs | 21 +++++++++++++++++++-- tests/cli.rs | 36 ++++++++++++++++++++++++++++++++++-- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fa6c51..ffc28bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,10 +526,17 @@ dependencies = [ "nom", "once_cell", "rexpect", + "shlex", "tempfile", "thiserror", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 81d8115..f2a906a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ clap = { version = "4.1.8", features = ["derive"] } git2 = { version = "0.16.1", default-features = false, features = ["vendored-libgit2"] } nom = "7.1.3" rexpect = "0.5.0" +shlex = "1.1.0" thiserror = "1.0.38" [dev-dependencies] diff --git a/readme.org b/readme.org index 6d68020..fe09dc5 100644 --- a/readme.org +++ b/readme.org @@ -21,7 +21,7 @@ Pijul. - [X] Isolation of workdir between tests - [X] git fetch with git upload-pack - [X] git push with git receive-pack -- [ ] proper shell argument lexing, with quote stuff https://lib.rs/crates/shlex +- [X] proper shell argument lexing, with quote stuff https://lib.rs/crates/shlex - [ ] restrict repos to only acceptable paths - [ ] git init of shared repos - [ ] history (only within same session) https://lib.rs/crates/rustyline diff --git a/src/main.rs b/src/main.rs index 50f5127..bcbfe1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,8 +51,8 @@ fn run_command(user_input: String) -> Result, ShackleError> return Ok(ControlFlow::Break(())); } Ok(ShackleCommand::GitInit(GitInitArgs { repo_name })) => { - git::init(&repo_name)?; // TODO should report this error differently - println!("Successfully created {}.git", repo_name); + git::init(&repo_name)?; + println!("Successfully created \"{}.git\"", repo_name); } Ok(ShackleCommand::GitUploadPack(upload_pack_args)) => { let mut command = Command::new("git-upload-pack"); diff --git a/src/parser.rs b/src/parser.rs index 59dc7d8..89e0d76 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,5 +1,6 @@ use clap::{Parser, Subcommand}; use std::str::FromStr; +use thiserror::Error; #[derive(Parser, Clone, Debug, PartialEq, Eq)] #[command(name = "")] @@ -44,8 +45,16 @@ pub struct GitReceivePackArgs { pub directory: String, } +#[derive(Error, Debug)] +pub enum ParserError { + #[error(transparent)] + ClapError(#[from] clap::error::Error), + #[error("`{0}`")] + LexerError(String), +} + impl FromStr for ShackleCommand { - type Err = clap::error::Error; + type Err = ParserError; fn from_str(s: &str) -> Result { let trimmed = s.trim(); @@ -54,7 +63,15 @@ impl FromStr for ShackleCommand { } else if trimmed.len() == 0 { Ok(ShackleCommand::Whitespace) } else { - ShackleCommand::try_parse_from([""].into_iter().chain(trimmed.split_whitespace())) + let lexed = shlex::split(trimmed); + match lexed { + None => Err(ParserError::LexerError("Incomplete input".to_string())), + Some(lexed) => { + let parsed = + ShackleCommand::try_parse_from(["".to_owned()].into_iter().chain(lexed))?; + Ok(parsed) + } + } } } } diff --git a/tests/cli.rs b/tests/cli.rs index 6ace151..e6a59b3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -90,7 +90,7 @@ fn reports_error_with_nonsense_input() -> Result<()> { fn can_init_a_new_git_repo() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line("git-init my-new-repo")?; - c.p.exp_string("Successfully created my-new-repo.git")?; + c.p.exp_string("Successfully created \"my-new-repo.git\"")?; expect_prompt(&mut c.p)?; Command::new("git") @@ -106,7 +106,39 @@ fn can_init_a_new_git_repo() -> Result<()> { #[test] fn runs_a_single_command_and_exit_with_cli_flag() -> Result<()> { let mut c = run_batch_command("git-init another-new-repo")?; - c.p.exp_string("Successfully created another-new-repo.git")?; + c.p.exp_string("Successfully created \"another-new-repo.git\"")?; c.p.exp_eof()?; Ok(()) } + +#[test] +fn allows_quotes_arguments() -> Result<()> { + let mut c = spawn_interactive_process()?; + c.p.send_line("\"git-init\" 'another-new-repo'")?; + c.p.exp_string("Successfully created \"another-new-repo.git\"")?; + Ok(()) +} + +#[test] +fn errors_with_an_open_double_quote() -> Result<()> { + let mut c = spawn_interactive_process()?; + c.p.send_line("\"git-init 'another-new-repo'")?; + c.p.exp_string("Incomplete input")?; + Ok(()) +} + +#[test] +fn errors_with_an_open_single_quote() -> Result<()> { + let mut c = spawn_interactive_process()?; + c.p.send_line("'git-init 'another-new-repo'")?; + c.p.exp_string("Incomplete input")?; + Ok(()) +} + +#[test] +fn allows_single_quotes_and_spaces_inside_double_quotes() -> Result<()> { + let mut c = spawn_interactive_process()?; + c.p.send_line("git-init \"shukkie's new repo\"")?; + c.p.exp_string("Successfully created \"shukkie's new repo.git\"")?; + Ok(()) +} -- cgit v1.2.3