mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
integrate cmdexpand for windows
This commit is contained in:
parent
c174b01b8f
commit
131e44d7bf
2 changed files with 162 additions and 6 deletions
|
|
@ -30,7 +30,7 @@ yazi-prebuild = "^0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
clipboard-win = "^4"
|
clipboard-win = "^4"
|
||||||
cmdexpand = {git = "https://github.com/ndtoan96/cmdexpand.git", tag = "v0.1.0"}
|
nom = "^7"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "netbsd"))'.dependencies]
|
[target.'cfg(not(target_os = "netbsd"))'.dependencies]
|
||||||
trash = "^3"
|
trash = "^3"
|
||||||
|
|
|
||||||
166
core/src/external/shell.rs
vendored
166
core/src/external/shell.rs
vendored
|
|
@ -46,14 +46,11 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let args: Vec<String> = opt.args.iter().map(|s| s.to_string_lossy().to_string()).collect();
|
let args: Vec<String> = opt.args.iter().map(|s| s.to_string_lossy().to_string()).collect();
|
||||||
let cmd = cmdexpand::Expander::new(&opt.cmd.to_string_lossy().to_string())
|
let expanded_cmd = cmdexpand::expand_cmd(opt.cmd.to_string_lossy().as_ref(), &args)?;
|
||||||
.disable_context(true)
|
|
||||||
.add_args(&args)
|
|
||||||
.expand()?;
|
|
||||||
Ok(
|
Ok(
|
||||||
Command::new("cmd")
|
Command::new("cmd")
|
||||||
.arg("/C")
|
.arg("/C")
|
||||||
.arg(cmd)
|
.arg(expanded_cmd)
|
||||||
.stdin(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
.stdin(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
.stdout(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
.stdout(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
.stderr(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
.stderr(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
|
|
@ -62,3 +59,162 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod cmdexpand {
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use nom::branch::alt;
|
||||||
|
use nom::bytes::complete::{is_not, tag, take_while1};
|
||||||
|
use nom::character::complete::{anychar, char, digit1, space0, space1};
|
||||||
|
use nom::combinator::recognize;
|
||||||
|
use nom::multi::{many0, many1};
|
||||||
|
use nom::sequence::{delimited, pair, preceded, tuple};
|
||||||
|
use nom::IResult;
|
||||||
|
|
||||||
|
enum CommandPart<'a> {
|
||||||
|
Space(&'a str),
|
||||||
|
Text(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TextPart<'a> {
|
||||||
|
NormalText(&'a str),
|
||||||
|
PercentNumber(usize),
|
||||||
|
PercentStar,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
enum Quote {
|
||||||
|
DoubleQuote,
|
||||||
|
SingleQuote,
|
||||||
|
NoQuote,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_cmd<T>(cmd: &str, args: &[T]) -> Result<String>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
let parts = parse_cmd(cmd)?;
|
||||||
|
let mut expanded = String::new();
|
||||||
|
for part in parts {
|
||||||
|
match part {
|
||||||
|
CommandPart::Space(s) => expanded.push_str(s),
|
||||||
|
CommandPart::Text(text) => {
|
||||||
|
expanded.push_str(&expand_text(text, args)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escaped_char(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(pair(char('\\'), anychar))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cmd(cmd: &str) -> Result<Vec<CommandPart>> {
|
||||||
|
fn double_quote_text(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(delimited(char('"'), is_not("\""), char('"')))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn single_quote_text(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(delimited(char('\''), is_not("'"), char('\'')))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_quote_text(input: &str) -> IResult<&str, &str> {
|
||||||
|
take_while1(|c: char| !c.is_whitespace())(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, (leading_space, command_name, args, trailing_space)) = tuple((
|
||||||
|
space0,
|
||||||
|
alt((double_quote_text, single_quote_text, no_quote_text)),
|
||||||
|
many0(pair(space1, alt((double_quote_text, single_quote_text, no_quote_text)))),
|
||||||
|
space0,
|
||||||
|
))(cmd)
|
||||||
|
.map_err(|_| anyhow!("Cannot parse command `{cmd}`"))?;
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if !leading_space.is_empty() {
|
||||||
|
parts.push(CommandPart::Space(leading_space));
|
||||||
|
}
|
||||||
|
parts.push(CommandPart::Text(command_name));
|
||||||
|
for (space, arg) in args {
|
||||||
|
parts.push(CommandPart::Space(space));
|
||||||
|
parts.push(CommandPart::Text(arg));
|
||||||
|
}
|
||||||
|
if !trailing_space.is_empty() {
|
||||||
|
parts.push(CommandPart::Space(trailing_space));
|
||||||
|
}
|
||||||
|
Ok(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_text<T>(text: &str, args: &[T]) -> Result<String>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
let quote = if text.starts_with("\"") {
|
||||||
|
Quote::DoubleQuote
|
||||||
|
} else if text.starts_with("'") {
|
||||||
|
Quote::SingleQuote
|
||||||
|
} else {
|
||||||
|
Quote::NoQuote
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts = parse_text(text)?;
|
||||||
|
let mut expanded = String::new();
|
||||||
|
for part in parts {
|
||||||
|
match part {
|
||||||
|
TextPart::NormalText(s) => expanded.push_str(s),
|
||||||
|
TextPart::PercentNumber(i) => {
|
||||||
|
if i > 0 {
|
||||||
|
let replace_text = args
|
||||||
|
.get(i - 1)
|
||||||
|
.map(|content| preprocess(content.as_ref(), quote))
|
||||||
|
.unwrap_or_default();
|
||||||
|
expanded.push_str(&replace_text);
|
||||||
|
} else {
|
||||||
|
// Not sure what to do with %0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextPart::PercentStar => {
|
||||||
|
for (i, arg) in args.iter().enumerate() {
|
||||||
|
expanded.push_str(&preprocess(arg.as_ref(), quote));
|
||||||
|
if i + 1 < args.len() {
|
||||||
|
expanded.push_str(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_text(text: &str) -> Result<Vec<TextPart>> {
|
||||||
|
fn normal_text(input: &str) -> IResult<&str, TextPart> {
|
||||||
|
let (input, output) = recognize(many1(alt((escaped_char, is_not("\\%")))))(input)?;
|
||||||
|
Ok((input, TextPart::NormalText(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percent_star(input: &str) -> IResult<&str, TextPart> {
|
||||||
|
let (input, _) = tag("%*")(input)?;
|
||||||
|
Ok((input, TextPart::PercentStar))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percent_number(input: &str) -> IResult<&str, TextPart> {
|
||||||
|
let (input, output) = preceded(char('%'), digit1)(input)?;
|
||||||
|
let num: usize = output.parse().unwrap();
|
||||||
|
Ok((input, TextPart::PercentNumber(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, parts) = many0(alt((normal_text, percent_star, percent_number)))(text)
|
||||||
|
.map_err(|_| anyhow!("Cannot parse text `{text}`"))?;
|
||||||
|
Ok(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preprocess(content: &str, quote: Quote) -> String {
|
||||||
|
let inner_space = content.chars().any(|c| c.is_whitespace());
|
||||||
|
match (quote, inner_space) {
|
||||||
|
(Quote::NoQuote, true) => format!("\"{}\"", content.replace("\"", "\\\"")),
|
||||||
|
(Quote::NoQuote, false) => content.to_string(),
|
||||||
|
(Quote::SingleQuote, _) => content.replace("'", "\\'").to_string(),
|
||||||
|
(Quote::DoubleQuote, _) => content.replace("\"", "\\\"").to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue