From 633e68e73e5a287fbd373e24a4dd8de1eb425fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Fri, 7 Feb 2025 02:01:23 +0800 Subject: [PATCH] feat: support end of options (`--`) marker for all commands (#2298) --- Cargo.lock | 20 ++--- Cargo.toml | 3 +- yazi-proxy/Cargo.toml | 1 - yazi-proxy/src/options/plugin.rs | 2 +- yazi-proxy/src/options/search.rs | 2 +- yazi-shared/Cargo.toml | 1 - yazi-shared/src/event/cmd.rs | 2 +- yazi-shared/src/shell/mod.rs | 9 ++- yazi-shared/src/shell/unix.rs | 135 ++++++++++++++++++++++++++++++- 9 files changed, 150 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3107d406..b21506f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,9 +2191,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -2310,12 +2310,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "shlex" version = "1.3.0" @@ -2697,9 +2691,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -3107,9 +3101,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "7.0.1" +version = "7.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028" +checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" dependencies = [ "either", "env_home", @@ -3587,7 +3581,6 @@ version = "25.2.5" dependencies = [ "anyhow", "mlua", - "shell-words", "tokio", "yazi-config", "yazi-macro", @@ -3630,7 +3623,6 @@ dependencies = [ "percent-encoding", "ratatui", "serde", - "shell-words", "tokio", "uzers", "windows-sys 0.59.0", diff --git a/Cargo.toml b/Cargo.toml index 02003602..6ed62059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,11 +37,10 @@ regex = "1.11.1" scopeguard = "1.2.0" serde = { version = "1.0.217", features = [ "derive" ] } serde_json = "1.0.138" -shell-words = "1.1.0" tokio = { version = "1.43.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.13" -toml = { version = "0.8.19" } +toml = { version = "0.8.20" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.0", default-features = false, features = [ "std", "random", "xxhash3_128" ] } unicode-width = "0.2.0" diff --git a/yazi-proxy/Cargo.toml b/yazi-proxy/Cargo.toml index aa53f50a..0c646924 100644 --- a/yazi-proxy/Cargo.toml +++ b/yazi-proxy/Cargo.toml @@ -20,5 +20,4 @@ yazi-shared = { path = "../yazi-shared", version = "25.2.5" } # External dependencies anyhow = { workspace = true } mlua = { workspace = true } -shell-words = { workspace = true } tokio = { workspace = true } diff --git a/yazi-proxy/src/options/plugin.rs b/yazi-proxy/src/options/plugin.rs index f3dfd57b..2d84d467 100644 --- a/yazi-proxy/src/options/plugin.rs +++ b/yazi-proxy/src/options/plugin.rs @@ -27,7 +27,7 @@ impl TryFrom for PluginOpt { }; let args = if let Some(s) = c.str("args") { - Cmd::parse_args(shell_words::split(s)?.into_iter(), true)? + Cmd::parse_args(yazi_shared::shell::split_unix(s)?.into_iter(), true)? } else { Default::default() }; diff --git a/yazi-proxy/src/options/search.rs b/yazi-proxy/src/options/search.rs index f6d8575f..7f24f832 100644 --- a/yazi-proxy/src/options/search.rs +++ b/yazi-proxy/src/options/search.rs @@ -23,7 +23,7 @@ impl TryFrom for SearchOpt { Ok(Self { via, subject, - args: shell_words::split(c.str("args").unwrap_or_default()).map_err(|_| ())?, + args: yazi_shared::shell::split_unix(c.str("args").unwrap_or_default()).map_err(|_| ())?, args_raw: c.take_str("args").unwrap_or_default(), }) } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index e9c5bd88..78b6e087 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -21,7 +21,6 @@ parking_lot = { workspace = true } percent-encoding = "2.3.1" ratatui = { workspace = true } serde = { workspace = true } -shell-words = { workspace = true } tokio = { workspace = true } [target."cfg(unix)".dependencies] diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index 2ece1917..c5f7b916 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -165,7 +165,7 @@ impl FromStr for Cmd { #[allow(clippy::explicit_counter_loop)] fn from_str(s: &str) -> Result { - let mut args = shell_words::split(s)?; + let mut args = crate::shell::split_unix(s)?; if args.is_empty() || args[0].is_empty() { bail!("command name cannot be empty"); } diff --git a/yazi-shared/src/shell/mod.rs b/yazi-shared/src/shell/mod.rs index 02a3699d..a41de5f7 100644 --- a/yazi-shared/src/shell/mod.rs +++ b/yazi-shared/src/shell/mod.rs @@ -1,8 +1,9 @@ //! Escape characters that may have special meaning in a shell, including -//! spaces. This is a modified version of the [`shell-escape`] crate and [`this -//! PR`]. +//! spaces. This is a modified version of the [`shell-escape`], [`shell-words`] +//! and [`this PR`]. //! //! [`shell-escape`]: https://crates.io/crates/shell-escape +//! [`shell-words`]: https://crates.io/crates/shell-words //! [`this PR`]: https://github.com/sfackler/shell-escape/pull/9 use std::{borrow::Cow, ffi::OsStr}; @@ -41,7 +42,9 @@ pub fn escape_os_str(s: &OsStr) -> Cow { } #[inline] -pub fn split_unix(s: &str) -> anyhow::Result> { Ok(shell_words::split(s)?) } +pub fn split_unix(s: &str) -> anyhow::Result> { + unix::split(s).map_err(|()| anyhow::anyhow!("missing closing quote")) +} #[cfg(windows)] pub fn split_windows(s: &str) -> anyhow::Result> { Ok(windows::split(s)?) } diff --git a/yazi-shared/src/shell/unix.rs b/yazi-shared/src/shell/unix.rs index e578bf0e..56dfeb71 100644 --- a/yazi-shared/src/shell/unix.rs +++ b/yazi-shared/src/shell/unix.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, mem}; pub fn escape_str(s: &str) -> Cow { match escape_slice(s.as_bytes()) { @@ -46,6 +46,139 @@ fn allowed(b: u8) -> bool { matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'=' | b'/' | b',' | b'.' | b'+') } +pub fn split(s: &str) -> Result, ()> { + enum State { + /// Within a delimiter. + Delimiter, + /// After backslash, but before starting word. + Backslash, + /// Within an unquoted word. + Unquoted, + /// After backslash in an unquoted word. + UnquotedBackslash, + /// Within a single quoted word. + SingleQuoted, + /// Within a double quoted word. + DoubleQuoted, + /// After backslash inside a double quoted word. + DoubleQuotedBackslash, + /// Inside a comment. + Comment, + } + use State::*; + + let mut words = Vec::new(); + let mut word = String::new(); + let mut chars = s.chars(); + let mut state = Delimiter; + + macro_rules! flush { + () => { + if word == "--" { + words.push(chars.collect()); + break; + } + words.push(mem::take(&mut word)); + }; + } + + loop { + let c = chars.next(); + state = match state { + Delimiter => match c { + None => break, + Some('\'') => SingleQuoted, + Some('\"') => DoubleQuoted, + Some('\\') => Backslash, + Some('\t') | Some(' ') | Some('\n') => Delimiter, + Some('#') => Comment, + Some(c) => { + word.push(c); + Unquoted + } + }, + Backslash => match c { + None => { + word.push('\\'); + flush!(); + break; + } + Some('\n') => Delimiter, + Some(c) => { + word.push(c); + Unquoted + } + }, + Unquoted => match c { + None => { + flush!(); + break; + } + Some('\'') => SingleQuoted, + Some('\"') => DoubleQuoted, + Some('\\') => UnquotedBackslash, + Some('\t') | Some(' ') | Some('\n') => { + flush!(); + Delimiter + } + Some(c) => { + word.push(c); + Unquoted + } + }, + UnquotedBackslash => match c { + None => { + word.push('\\'); + flush!(); + break; + } + Some('\n') => Unquoted, + Some(c) => { + word.push(c); + Unquoted + } + }, + SingleQuoted => match c { + None => return Err(()), + Some('\'') => Unquoted, + Some(c) => { + word.push(c); + SingleQuoted + } + }, + DoubleQuoted => match c { + None => return Err(()), + Some('\"') => Unquoted, + Some('\\') => DoubleQuotedBackslash, + Some(c) => { + word.push(c); + DoubleQuoted + } + }, + DoubleQuotedBackslash => match c { + None => return Err(()), + Some('\n') => DoubleQuoted, + Some(c @ '$') | Some(c @ '`') | Some(c @ '"') | Some(c @ '\\') => { + word.push(c); + DoubleQuoted + } + Some(c) => { + word.push('\\'); + word.push(c); + DoubleQuoted + } + }, + Comment => match c { + None => break, + Some('\n') => Delimiter, + Some(_) => Comment, + }, + } + } + + Ok(words) +} + #[cfg(test)] mod tests { use super::*;