feat: support end of options (--) marker for all commands (#2298)

This commit is contained in:
三咲雅 · Misaki Masa 2025-02-07 02:01:23 +08:00 committed by GitHub
parent b1d2be067f
commit 633e68e73e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 150 additions and 25 deletions

20
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 }

View file

@ -27,7 +27,7 @@ impl TryFrom<CmdCow> 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()
};

View file

@ -23,7 +23,7 @@ impl TryFrom<CmdCow> 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(),
})
}

View file

@ -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]

View file

@ -165,7 +165,7 @@ impl FromStr for Cmd {
#[allow(clippy::explicit_counter_loop)]
fn from_str(s: &str) -> Result<Self, Self::Err> {
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");
}

View file

@ -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<OsStr> {
}
#[inline]
pub fn split_unix(s: &str) -> anyhow::Result<Vec<String>> { Ok(shell_words::split(s)?) }
pub fn split_unix(s: &str) -> anyhow::Result<Vec<String>> {
unix::split(s).map_err(|()| anyhow::anyhow!("missing closing quote"))
}
#[cfg(windows)]
pub fn split_windows(s: &str) -> anyhow::Result<Vec<String>> { Ok(windows::split(s)?) }

View file

@ -1,4 +1,4 @@
use std::borrow::Cow;
use std::{borrow::Cow, mem};
pub fn escape_str(s: &str) -> Cow<str> {
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<Vec<String>, ()> {
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::*;