Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 1 | use std::collections::HashMap; |
| 2 | use std::env; |
| 3 | use std::fmt; |
| 4 | use std::process::exit; |
| 5 | |
| 6 | use crate::flags::{FlagParseError, Flags, ParseOutcome}; |
| 7 | use crate::util::*; |
| 8 | |
| 9 | #[derive(Debug)] |
| 10 | pub(crate) enum OptionError { |
| 11 | FlagError(FlagParseError), |
| 12 | Generic(String), |
| 13 | } |
| 14 | |
| 15 | impl 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)] |
| 25 | pub(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 | |
| 43 | pub(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 | |
| 165 | fn 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 | |
| 174 | fn 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 | |
| 188 | fn 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 | |
| 199 | fn 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 | } |