use anyhow::{Context, Result};
use std::ffi::{OsStr, OsString};
pub fn expand() -> Result<Vec<OsString>> {
let mut expander = Expander::default();
for arg in std::env::args_os() {
expander.push(arg)?;
}
Ok(expander.args)
}
#[derive(Default)]
struct Expander {
args: Vec<OsString>,
}
impl Expander {
fn push(&mut self, arg: OsString) -> Result<()> {
let bytes = arg.as_encoded_bytes();
match bytes.split_first() {
Some((b'@', rest)) => {
self.push_file(unsafe { OsStr::from_encoded_bytes_unchecked(rest) })
}
_ => {
self.args.push(arg);
Ok(())
}
}
}
fn push_file(&mut self, file: &OsStr) -> Result<()> {
let contents =
std::fs::read_to_string(file).with_context(|| format!("failed to read {file:?}"))?;
for part in imp::split(&contents) {
self.push(part.into())?;
}
Ok(())
}
}
#[cfg(not(windows))]
use gnu as imp;
#[cfg(not(windows))]
mod gnu {
pub fn split(s: &str) -> impl Iterator<Item = String> + '_ {
Split { iter: s.chars() }
}
struct Split<'a> {
iter: std::str::Chars<'a>,
}
impl<'a> Iterator for Split<'a> {
type Item = String;
fn next(&mut self) -> Option<String> {
loop {
match self.iter.next()? {
c if c.is_whitespace() => {}
'"' => break Some(self.quoted('"')),
'\'' => break Some(self.quoted('\'')),
c => {
let mut ret = String::new();
self.push(&mut ret, c);
while let Some(next) = self.iter.next() {
if next.is_whitespace() {
break;
}
self.push(&mut ret, next);
}
break Some(ret);
}
}
}
}
}
impl Split<'_> {
fn quoted(&mut self, end: char) -> String {
let mut part = String::new();
while let Some(next) = self.iter.next() {
if next == end {
break;
}
self.push(&mut part, next);
}
part
}
fn push(&mut self, dst: &mut String, ch: char) {
if ch == '\\' {
if let Some(ch) = self.iter.next() {
dst.push(ch);
return;
}
}
dst.push(ch);
}
}
#[test]
fn tests() {
assert_eq!(split("x").collect::<Vec<_>>(), ["x"]);
assert_eq!(split("\\x").collect::<Vec<_>>(), ["x"]);
assert_eq!(split("'x'").collect::<Vec<_>>(), ["x"]);
assert_eq!(split("\"x\"").collect::<Vec<_>>(), ["x"]);
assert_eq!(split("x y").collect::<Vec<_>>(), ["x", "y"]);
assert_eq!(split("x\ny").collect::<Vec<_>>(), ["x", "y"]);
assert_eq!(split("\\x y").collect::<Vec<_>>(), ["x", "y"]);
assert_eq!(split("'x y'").collect::<Vec<_>>(), ["x y"]);
assert_eq!(split("\"x y\"").collect::<Vec<_>>(), ["x y"]);
assert_eq!(split("\"x 'y'\"\n'y'").collect::<Vec<_>>(), ["x 'y'", "y"]);
assert_eq!(
split(
r#"
a\ \\b
z
"x y \\z"
"#
)
.collect::<Vec<_>>(),
["a \\b", "z", "x y \\z"]
);
}
}
#[cfg(windows)]
use windows as imp;
#[cfg(windows)]
mod windows {
pub fn split(s: &str) -> impl Iterator<Item = String> {
winsplit::split(s).into_iter()
}
}