blob: 24bba9fe80606b721d05608987c7b9721b02c843 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001use std::collections::HashMap;
2use std::env;
3use std::fmt;
4use std::process::exit;
5
6use crate::flags::{FlagParseError, Flags, ParseOutcome};
7use crate::util::*;
8
9#[derive(Debug)]
10pub(crate) enum OptionError {
11 FlagError(FlagParseError),
12 Generic(String),
13}
14
15impl fmt::Display for OptionError {
16 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
17 match self {
18 Self::FlagError(e) => write!(f, "error parsing flags: {}", e),
19 Self::Generic(s) => write!(f, "{}", s),
20 }
21 }
22}
23
24#[derive(Debug)]
25pub(crate) struct Options {
26 // Contains the path to the child executable
27 pub(crate) executable: String,
28 // Contains arguments for the child process fetched from files.
29 pub(crate) child_arguments: Vec<String>,
30 // Contains environment variables for the child process fetched from files.
31 pub(crate) child_environment: HashMap<String, String>,
32 // If set, create the specified file after the child process successfully
33 // terminated its execution.
34 pub(crate) touch_file: Option<String>,
35 // If set to (source, dest) copies the source file to dest.
36 pub(crate) copy_output: Option<(String, String)>,
37 // If set, redirects the child process stdout to this file.
38 pub(crate) stdout_file: Option<String>,
39 // If set, redirects the child process stderr to this file.
40 pub(crate) stderr_file: Option<String>,
41}
42
43pub(crate) fn options() -> Result<Options, OptionError> {
44 // Process argument list until -- is encountered.
45 // Everything after is sent to the child process.
46 let mut subst_mapping_raw = None;
47 let mut volatile_status_file_raw = None;
48 let mut env_file_raw = None;
49 let mut arg_file_raw = None;
50 let mut touch_file = None;
51 let mut copy_output_raw = None;
52 let mut stdout_file = None;
53 let mut stderr_file = None;
54 let mut flags = Flags::new();
55 flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw);
56 flags.define_flag("--volatile-status-file", "", &mut volatile_status_file_raw);
57 flags.define_repeated_flag(
58 "--env-file",
59 "File(s) containing environment variables to pass to the child process.",
60 &mut env_file_raw,
61 );
62 flags.define_repeated_flag(
63 "--arg-file",
64 "File(s) containing command line arguments to pass to the child process.",
65 &mut arg_file_raw,
66 );
67 flags.define_flag(
68 "--touch-file",
69 "Create this file after the child process runs successfully.",
70 &mut touch_file,
71 );
72 flags.define_repeated_flag("--copy-output", "", &mut copy_output_raw);
73 flags.define_flag(
74 "--stdout-file",
75 "Redirect subprocess stdout in this file.",
76 &mut stdout_file,
77 );
78 flags.define_flag(
79 "--stderr-file",
80 "Redirect subprocess stderr in this file.",
81 &mut stderr_file,
82 );
83
84 let mut child_args = match flags
85 .parse(env::args().collect())
86 .map_err(OptionError::FlagError)?
87 {
88 ParseOutcome::Help(help) => {
89 eprintln!("{}", help);
90 exit(0);
91 }
92 ParseOutcome::Parsed(p) => p,
93 };
94 let current_dir = std::env::current_dir()
95 .map_err(|e| OptionError::Generic(format!("failed to get current directory: {}", e)))?
96 .to_str()
97 .ok_or_else(|| OptionError::Generic("current directory not utf-8".to_owned()))?
98 .to_owned();
99 let subst_mappings = subst_mapping_raw
100 .unwrap_or_default()
101 .into_iter()
102 .map(|arg| {
103 let (key, val) = arg.split_once('=').ok_or_else(|| {
104 OptionError::Generic(format!("empty key for substitution '{}'", arg))
105 })?;
106 let v = if val == "${pwd}" {
107 current_dir.as_str()
108 } else {
109 val
110 }
111 .to_owned();
112 Ok((key.to_owned(), v))
113 })
114 .collect::<Result<Vec<(String, String)>, OptionError>>()?;
115 let stamp_mappings =
116 volatile_status_file_raw.map_or_else(Vec::new, |s| read_stamp_status_to_array(s).unwrap());
117
118 let environment_file_block = env_from_files(env_file_raw.unwrap_or_default())?;
119 let mut file_arguments = args_from_file(arg_file_raw.unwrap_or_default())?;
120 // Process --copy-output
121 let copy_output = copy_output_raw
122 .map(|co| {
123 if co.len() != 2 {
124 return Err(OptionError::Generic(format!(
125 "\"--copy-output\" needs exactly 2 parameters, {} provided",
126 co.len()
127 )));
128 }
129 let copy_source = &co[0];
130 let copy_dest = &co[1];
131 if copy_source == copy_dest {
132 return Err(OptionError::Generic(format!(
133 "\"--copy-output\" source ({}) and dest ({}) need to be different.",
134 copy_source, copy_dest
135 )));
136 }
137 Ok((copy_source.to_owned(), copy_dest.to_owned()))
138 })
139 .transpose()?;
140
141 // Prepare the environment variables, unifying those read from files with the ones
142 // of the current process.
143 let vars = environment_block(environment_file_block, &stamp_mappings, &subst_mappings);
144 // Append all the arguments fetched from files to those provided via command line.
145 child_args.append(&mut file_arguments);
146 let child_args = prepare_args(child_args, &subst_mappings);
147 // Split the executable path from the rest of the arguments.
148 let (exec_path, args) = child_args.split_first().ok_or_else(|| {
149 OptionError::Generic(
150 "at least one argument after -- is required (the child process path)".to_owned(),
151 )
152 })?;
153
154 Ok(Options {
155 executable: exec_path.to_owned(),
156 child_arguments: args.to_vec(),
157 child_environment: vars,
158 touch_file,
159 copy_output,
160 stdout_file,
161 stderr_file,
162 })
163}
164
165fn args_from_file(paths: Vec<String>) -> Result<Vec<String>, OptionError> {
166 let mut args = vec![];
167 for path in paths.into_iter() {
168 let mut lines = read_file_to_array(path).map_err(OptionError::Generic)?;
169 args.append(&mut lines);
170 }
171 Ok(args)
172}
173
174fn env_from_files(paths: Vec<String>) -> Result<HashMap<String, String>, OptionError> {
175 let mut env_vars = HashMap::new();
176 for path in paths.into_iter() {
177 let lines = read_file_to_array(path).map_err(OptionError::Generic)?;
178 for line in lines.into_iter() {
179 let (k, v) = line
180 .split_once('=')
181 .ok_or_else(|| OptionError::Generic("environment file invalid".to_owned()))?;
182 env_vars.insert(k.to_owned(), v.to_owned());
183 }
184 }
185 Ok(env_vars)
186}
187
188fn prepare_args(mut args: Vec<String>, subst_mappings: &[(String, String)]) -> Vec<String> {
189 for (f, replace_with) in subst_mappings {
190 for arg in args.iter_mut() {
191 let from = format!("${{{}}}", f);
192 let new = arg.replace(from.as_str(), replace_with);
193 *arg = new;
194 }
195 }
196 args
197}
198
199fn environment_block(
200 environment_file_block: HashMap<String, String>,
201 stamp_mappings: &[(String, String)],
202 subst_mappings: &[(String, String)],
203) -> HashMap<String, String> {
204 // Taking all environment variables from the current process
205 // and sending them down to the child process
206 let mut environment_variables: HashMap<String, String> = std::env::vars().collect();
207 // Have the last values added take precedence over the first.
208 // This is simpler than needing to track duplicates and explicitly override
209 // them.
210 environment_variables.extend(environment_file_block.into_iter());
211 for (f, replace_with) in stamp_mappings {
212 for value in environment_variables.values_mut() {
213 let from = format!("{{{}}}", f);
214 let new = value.replace(from.as_str(), replace_with);
215 *value = new;
216 }
217 }
218 for (f, replace_with) in subst_mappings {
219 for value in environment_variables.values_mut() {
220 let from = format!("${{{}}}", f);
221 let new = value.replace(from.as_str(), replace_with);
222 *value = new;
223 }
224 }
225 environment_variables
226}