integrate cmdexpand for windows

This commit is contained in:
Nguyen Duc Toan 2023-09-23 15:17:39 +07:00 committed by sxyazi
parent c174b01b8f
commit 131e44d7bf
No known key found for this signature in database
2 changed files with 162 additions and 6 deletions

View file

@ -30,7 +30,7 @@ yazi-prebuild = "^0"
[target.'cfg(target_os = "windows")'.dependencies]
clipboard-win = "^4"
cmdexpand = {git = "https://github.com/ndtoan96/cmdexpand.git", tag = "v0.1.0"}
nom = "^7"
[target.'cfg(not(target_os = "netbsd"))'.dependencies]
trash = "^3"

View file

@ -46,14 +46,11 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
#[cfg(target_os = "windows")]
{
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())
.disable_context(true)
.add_args(&args)
.expand()?;
let expanded_cmd = cmdexpand::expand_cmd(opt.cmd.to_string_lossy().as_ref(), &args)?;
Ok(
Command::new("cmd")
.arg("/C")
.arg(cmd)
.arg(expanded_cmd)
.stdin(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() })
@ -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(),
}
}
}