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 {}