Skip to content

Commit

Permalink
fix: handle shell arguments on Windows (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndtoan96 authored and sxyazi committed Oct 4, 2023
1 parent b48edfd commit 77988d4
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 15 deletions.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

177 changes: 166 additions & 11 deletions core/src/external/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,170 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
);

#[cfg(windows)]
return Ok(
Command::new("cmd")
.stdin(opt.stdio())
.stdout(opt.stdio())
.stderr(opt.stdio())
.arg("/C")
.arg(opt.cmd)
.args(opt.args)
.kill_on_drop(true)
.spawn()?,
);
{
let args: Vec<String> = opt.args.iter().map(|s| s.to_string_lossy().to_string()).collect();
let args_: Vec<&str> = args.iter().map(|s| s.as_ref()).collect();
let expanded = parser::parse(opt.cmd.to_string_lossy().as_ref(), &args_);
Ok(
Command::new("cmd")
.arg("/C")
.args(&expanded)
.stdin(opt.stdio())
.stdout(opt.stdio())
.stderr(opt.stdio())
.kill_on_drop(!opt.orphan)
.spawn()?,
)
}
}

#[cfg(windows)]
mod parser {
use std::{iter::Peekable, str::Chars};

pub(super) fn parse(cmd: &str, args: &[&str]) -> Vec<String> {
let mut it = cmd.chars().peekable();
let mut expanded = Vec::new();

while let Some(c) = it.next() {
if c.is_whitespace() {
continue;
}
let mut s = String::new();

if c == '\'' {
while let Some(c) = it.next() {
if c == '\'' {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
} else if c == '"' {
while let Some(c) = it.next() {
if c == '"' {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
} else if c == '%' && it.peek().is_some_and(|&c| c == '*') {
it.next();
for arg in args {
expanded.push(arg.to_string());
}
} else {
next_string(&mut it, args, &mut s, c);

while let Some(c) = it.next() {
if c.is_whitespace() {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
}
}

expanded
}

fn next_string(it: &mut Peekable<Chars<'_>>, args: &[&str], s: &mut String, c: char) {
if c == '\\' {
match it.next() {
Some('\\') => s.push('\\'), // \\ ==> \
Some('\'') => s.push('\''), // \' ==> '
Some('"') => s.push('"'), // \" ==> "
Some('%') => s.push('%'), // \% ==> %
Some('n') => s.push('\n'), // \n ==> '\n'
Some('t') => s.push('\t'), // \t ==> '\t'
Some('r') => s.push('\r'), // \r ==> '\r'
Some(c) => {
s.push('\\');
s.push(c);
}
None => s.push('\\'),
}
} else if c == '%' {
match it.peek() {
Some('*') => {
s.push_str(&args.join(" "));
it.next();
}
Some(n) if n.is_ascii_digit() => {
let mut pos = n.to_string();

it.next();
while let Some(&n) = it.peek() {
if n.is_ascii_digit() {
pos.push(it.next().unwrap());
} else {
break;
}
}

let pos = pos.parse::<usize>().unwrap();
if pos > 0 {
s.push_str(args.get(pos - 1).unwrap_or(&""));
}
}
_ => s.push('%'),
}
} else {
s.push(c);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_no_quote() {
let args = parse("echo abc xyz %0 %2", &["111", "222"]);
assert_eq!(args, ["echo", "abc", "xyz", "", "222"]);

let args = parse(" echo abc xyz %1 %2 ", &["111", "222"]);
assert_eq!(args, ["echo", "abc", "xyz", "111", "222"]);
}

#[test]
fn test_single_quote() {
let args = parse("echo 'abc xyz' '%1' %2", &["111", "222"]);
assert_eq!(args, ["echo", "abc xyz", "111", "222"]);

let args = parse("echo 'abc \"\"xyz' '%1' %2", &["111", "222"]);
assert_eq!(args, ["echo", "abc \"\"xyz", "111", "222"]);
}

#[test]
fn test_double_quote() {
let args = parse("echo \"abc ' 'xyz\" \"%1\" %2 %3", &["111", "222"]);
assert_eq!(args, ["echo", "abc ' 'xyz", "111", "222", ""]);
}

#[test]
fn test_escaped() {
let args = parse("echo \"a\tbc ' 'x\nyz\" \"\\%1\" %2 %3", &["111", "22 2"]);
assert_eq!(args, ["echo", "a\tbc ' 'x\nyz", "%1", "22 2", ""]);
}

#[test]
fn test_percent_star() {
let args = parse("echo %* xyz", &["111", "222"]);
assert_eq!(args, ["echo", "111", "222", "xyz"]);

let args = parse("echo '%*' xyz", &["111", "222"]);
assert_eq!(args, ["echo", "111 222", "xyz"]);

let args = parse("echo -C%* xyz", &["111", "222"]);
assert_eq!(args, ["echo", "-C111 222", "xyz"]);
}

#[test]
fn test_env_var() {
let args = parse(" %EDITOR% %* xyz", &["111", "222"]);
assert_eq!(args, ["%EDITOR%", "111", "222", "xyz"]);
}
}
}

0 comments on commit 77988d4

Please sign in to comment.