1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
use std::{env, error, fmt, fs, io};
use rustc_session::EarlyDiagCtxt;
use rustc_span::ErrorGuaranteed;
/// Expands argfiles in command line arguments.
#[derive(Default)]
struct Expander {
shell_argfiles: bool,
next_is_unstable_option: bool,
expanded: Vec<String>,
}
impl Expander {
/// Handles the next argument. If the argument is an argfile, it is expanded
/// inline.
fn arg(&mut self, arg: &str) -> Result<(), Error> {
if let Some(argfile) = arg.strip_prefix('@') {
match argfile.split_once(':') {
Some(("shell", path)) if self.shell_argfiles => {
shlex::split(&Self::read_file(path)?)
.ok_or_else(|| Error::ShellParseError(path.to_string()))?
.into_iter()
.for_each(|arg| self.push(arg));
}
_ => {
let contents = Self::read_file(argfile)?;
contents.lines().for_each(|arg| self.push(arg.to_string()));
}
}
} else {
self.push(arg.to_string());
}
Ok(())
}
/// Adds a command line argument verbatim with no argfile expansion.
fn push(&mut self, arg: String) {
// Unfortunately, we have to do some eager argparsing to handle unstable
// options which change the behavior of argfile arguments.
//
// Normally, all of the argfile arguments (e.g. `@args.txt`) are
// expanded into our arguments list *and then* the whole list of
// arguments are passed on to be parsed. However, argfile parsing
// options like `-Zshell_argfiles` need to change the behavior of that
// argument expansion. So we have to do a little parsing on our own here
// instead of leaning on the existing logic.
//
// All we care about are unstable options, so we parse those out and
// look for any that affect how we expand argfiles. This argument
// inspection is very conservative; we only change behavior when we see
// exactly the options we're looking for and everything gets passed
// through.
if self.next_is_unstable_option {
self.inspect_unstable_option(&arg);
self.next_is_unstable_option = false;
} else if let Some(unstable_option) = arg.strip_prefix("-Z") {
if unstable_option.is_empty() {
self.next_is_unstable_option = true;
} else {
self.inspect_unstable_option(unstable_option);
}
}
self.expanded.push(arg);
}
/// Consumes the `Expander`, returning the expanded arguments.
fn finish(self) -> Vec<String> {
self.expanded
}
/// Parses any relevant unstable flags specified on the command line.
fn inspect_unstable_option(&mut self, option: &str) {
match option {
"shell-argfiles" => self.shell_argfiles = true,
_ => (),
}
}
/// Reads the contents of a file as UTF-8.
fn read_file(path: &str) -> Result<String, Error> {
fs::read_to_string(path).map_err(|e| {
if e.kind() == io::ErrorKind::InvalidData {
Error::Utf8Error(path.to_string())
} else {
Error::IOError(path.to_string(), e)
}
})
}
}
/// Replaces any `@file` arguments with the contents of `file`, with each line of `file` as a
/// separate argument.
///
/// **Note:** This function doesn't interpret argument 0 in any special way.
/// If this function is intended to be used with command line arguments,
/// `argv[0]` must be removed prior to calling it manually.
#[allow(rustc::untranslatable_diagnostic)] // FIXME: make this translatable
pub fn arg_expand_all(
early_dcx: &EarlyDiagCtxt,
at_args: &[String],
) -> Result<Vec<String>, ErrorGuaranteed> {
let mut expander = Expander::default();
let mut result = Ok(());
for arg in at_args {
if let Err(err) = expander.arg(arg) {
result = Err(early_dcx.early_err(format!("failed to load argument file: {err}")));
}
}
result.map(|()| expander.finish())
}
/// Gets the raw unprocessed command-line arguments as Unicode strings, without doing any further
/// processing (e.g., without `@file` expansion).
///
/// This function is identical to [`env::args()`] except that it emits an error when it encounters
/// non-Unicode arguments instead of panicking.
pub fn raw_args(early_dcx: &EarlyDiagCtxt) -> Result<Vec<String>, ErrorGuaranteed> {
let mut res = Ok(Vec::new());
for (i, arg) in env::args_os().enumerate() {
match arg.into_string() {
Ok(arg) => {
if let Ok(args) = &mut res {
args.push(arg);
}
}
Err(arg) => {
res =
Err(early_dcx.early_err(format!("argument {i} is not valid Unicode: {arg:?}")))
}
}
}
res
}
#[derive(Debug)]
enum Error {
Utf8Error(String),
IOError(String, io::Error),
ShellParseError(String),
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Utf8Error(path) => write!(fmt, "UTF-8 error in {path}"),
Error::IOError(path, err) => write!(fmt, "IO error: {path}: {err}"),
Error::ShellParseError(path) => write!(fmt, "invalid shell-style arguments in {path}"),
}
}
}
impl error::Error for Error {}