Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle shell arguments on Windows #214

Merged
merged 9 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

175 changes: 164 additions & 11 deletions core/src/external/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,168 @@ 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 == '\'' {
s.clear();
while let Some(c) = it.next() {
if c == '\'' {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
} else if c == '"' {
s.clear();
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 {
s.clear();
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;
}
}
s.push_str(args.get(pos.parse::<usize>().unwrap() - 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 %1 %2", &["111", "222"]);
assert_eq!(args, vec!["echo", "abc", "xyz", "111", "222"]);

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

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

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

#[test]
fn test_double_quote() {
let args = parse("echo \"abc ' 'xyz\" \"%1\" %2 %3", &["111", "222"]);
assert_eq!(args, vec!["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, vec!["echo", "a\tbc ' 'x\nyz", "%1", "22 2", ""]);
}

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

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

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

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