this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: rewrite cli parsing

Still needs tests to validate, but mostly works in manual testing.

+178 -114
+20 -12
src/gzip.rs
··· 6 6 7 7 pub struct Gzip { 8 8 pub compression_level: u32, 9 - pub common_args: CmprssCommonArgs, 10 9 } 11 10 12 11 impl Default for Gzip { 13 12 fn default() -> Self { 14 13 Gzip { 15 14 compression_level: 6, 16 - common_args: Default::default(), 17 15 } 18 16 } 19 17 } ··· 29 27 "gzip" 30 28 } 31 29 32 - fn common_args(&self) -> &CmprssCommonArgs { 33 - &self.common_args 34 - } 35 - 36 30 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 31 + if let CmprssOutput::Path(out_path) = &output { 32 + if out_path.is_dir() { 33 + return cmprss_error("Gzip does not support compressing to a directory. Please specify an output file."); 34 + } 35 + } 36 + if let CmprssInput::Path(input_paths) = &input { 37 + for x in input_paths { 38 + if x.is_dir() { 39 + return cmprss_error( 40 + "Gzip does not support compressing a directory. Please specify only files.", 41 + ); 42 + } 43 + } 44 + } 37 45 match (input, output) { 38 46 (CmprssInput::Path(in_path), CmprssOutput::Path(out_path)) => { 39 47 let mut encoder = GzEncoder::new( ··· 70 78 if in_path.len() > 1 { 71 79 return cmprss_error("only 1 archive can be extracted at a time"); 72 80 } 73 - self.extract_internal(File::open(in_path[0])?, File::create(out_path)?) 81 + self.extract_internal(File::open(in_path[0].as_path())?, File::create(out_path)?) 74 82 } 75 83 (CmprssInput::Path(in_path), CmprssOutput::Pipe(out_pipe)) => { 76 84 if in_path.len() > 1 { 77 85 return cmprss_error("only 1 archive can be extracted at a time"); 78 86 } 79 - self.extract_internal(File::open(in_path[0])?, out_pipe) 87 + self.extract_internal(File::open(in_path[0].as_path())?, out_pipe) 80 88 } 81 89 (CmprssInput::Pipe(in_pipe), CmprssOutput::Path(out_path)) => { 82 90 self.extract_internal(in_pipe, File::create(out_path)?) ··· 132 140 133 141 // Roundtrip compress/extract 134 142 compressor.compress( 135 - CmprssInput::Path(vec![file.path()]), 136 - CmprssOutput::Path(archive.path()), 143 + CmprssInput::Path(vec![file.path().to_path_buf()]), 144 + CmprssOutput::Path(archive.path().to_path_buf()), 137 145 )?; 138 146 archive.assert(predicate::path::is_file()); 139 147 compressor.extract( 140 - CmprssInput::Path(vec![archive.path()]), 141 - CmprssOutput::Path(working_dir.child("test.txt").path()), 148 + CmprssInput::Path(vec![archive.path().to_path_buf()]), 149 + CmprssOutput::Path(working_dir.child("test.txt").path().to_path_buf()), 142 150 )?; 143 151 144 152 // Assert the files are identical
+133 -68
src/main.rs
··· 5 5 use clap::{Args, Parser, Subcommand}; 6 6 use is_terminal::IsTerminal; 7 7 use std::io; 8 - use std::path::Path; 8 + use std::path::{Path, PathBuf}; 9 9 use utils::*; 10 10 11 11 /// A compression multi-tool ··· 50 50 #[derive(Args, Debug)] 51 51 struct CommonArgs { 52 52 /// Input/Output file/directory 53 - #[arg(index = 1)] 53 + #[arg(short, long)] 54 54 input: Option<String>, 55 55 56 56 /// Output file/directory 57 - #[arg(index = 2)] 57 + #[arg(short, long)] 58 58 output: Option<String>, 59 59 60 60 /// Compress the input (default) ··· 64 64 /// Extract the input 65 65 #[arg(short, long)] 66 66 extract: bool, 67 - } 68 67 69 - impl CommonArgs { 70 - /// Convert clap argument struct into utils::CmprssCommonArgs 71 - /// This is done, perhaps unnecessarily, to keep clap out of the lib 72 - fn into_common(self) -> CmprssCommonArgs { 73 - CmprssCommonArgs { 74 - compress: self.compress, 75 - input: self.input, 76 - output: self.output, 77 - extract: self.extract, 78 - } 79 - } 68 + /// List of I/O 69 + /// This consists of all the inputs followed by the single output, with intelligent fallback to stdin/stdout. 70 + #[arg()] 71 + io_list: Vec<String>, 80 72 } 81 73 82 74 #[derive(Args, Debug)] ··· 103 95 } 104 96 105 97 /// Get the default output filename or return error if the input isn't specified 98 + #[allow(dead_code)] 106 99 fn get_default_output<T: Compressor>( 107 100 compressor: &T, 108 101 input: &Option<String>, ··· 123 116 } 124 117 } 125 118 126 - fn command<T: Compressor>(compressor: T) -> Result<(), io::Error> { 127 - let args = compressor.common_args(); 128 - // Use to provide a longer lifetime for this value 129 - let default_output; 130 - // Input prefers stdin if that is a pipe, and falls back to reading from a file. 131 - let input = match std::io::stdin().is_terminal() { 132 - false => CmprssInput::Pipe(std::io::stdin()), 133 - true => { 134 - // stdin isn't a pipe, need to read from a file 135 - CmprssInput::Path(vec![Path::new(get_input_filename(&args.input)?)]) 119 + #[derive(Debug)] 120 + enum Action { 121 + Compress, 122 + Extract, 123 + } 124 + 125 + /// Defines a single compress/extract action to take. 126 + #[derive(Debug)] 127 + struct Job { 128 + input: CmprssInput, 129 + output: CmprssOutput, 130 + action: Action, 131 + } 132 + 133 + /// Parse the common args and determine the details of the job requested 134 + fn get_job(common_args: &CommonArgs) -> Result<Job, io::Error> { 135 + let action = { 136 + if common_args.compress { 137 + Action::Compress 138 + } else if common_args.extract { 139 + Action::Extract 140 + } else { 141 + Action::Compress 142 + } 143 + }; 144 + 145 + let mut inputs = match &common_args.input { 146 + Some(input) => { 147 + let path = PathBuf::from(input); 148 + if !path.try_exists()? { 149 + return Err(io::Error::new( 150 + io::ErrorKind::Other, 151 + "Specified input path does not exist", 152 + )); 153 + } 154 + vec![path] 136 155 } 156 + None => Vec::new(), 137 157 }; 138 - // Output prefers the stdout if we're piping, and falls back to piping to a file. 139 - // TODO: Not sure that this output logic is the right thing to do 140 - // TODO: Properly handle the output file 141 - // Fail/Warn on existence 142 - // Remove if you've created a stub 143 - let output = match std::io::stdout().is_terminal() { 144 - false => CmprssOutput::Pipe(std::io::stdout()), 145 - true => { 146 - if args.output.is_none() { 147 - if !std::io::stdin().is_terminal() && args.input.is_some() { 148 - // Stdin is being used as the input, so use the 'input' file as the output 149 - CmprssOutput::Path(Path::new(get_input_filename(&args.input)?)) 158 + let mut output = match &common_args.output { 159 + Some(output) => { 160 + let path = Path::new(output); 161 + if path.try_exists()? && !path.is_dir() { 162 + // Output path exists, bail out 163 + return Err(io::Error::new( 164 + io::ErrorKind::Other, 165 + "Specified output path already exists", 166 + )); 167 + } 168 + Some(path) 169 + } 170 + None => None, 171 + }; 172 + 173 + // Process the io_list, check if there is an output first 174 + let mut io_list = common_args.io_list.clone(); 175 + if output.is_none() { 176 + if let Some(possible_output) = common_args.io_list.last() { 177 + let path = Path::new(possible_output); 178 + if !path.try_exists()? { 179 + // Use the given path if it doesn't exist 180 + output = Some(path); 181 + io_list.pop(); 182 + } else if path.is_dir() { 183 + if std::io::stdout().is_terminal() { 184 + // stdout isn't a pipe, so use this directory as output 185 + output = Some(path); 186 + io_list.pop(); 150 187 } else { 151 - default_output = get_default_output(&compressor, &args.input, args.extract)?; 152 - CmprssOutput::Path(Path::new(&default_output)) 188 + // this is a directory and stdout is a pipe 189 + // TODO: This may need to ask the user, for now assume stdout is output 153 190 } 154 191 } else { 155 - CmprssOutput::Path(Path::new(args.output.as_ref().unwrap())) 192 + // TODO: append checks 156 193 } 157 194 } 195 + } 196 + // Validate the specified inputs 197 + // Everything in the io_list should be an input 198 + for input in &io_list { 199 + let path = PathBuf::from(input); 200 + if !path.try_exists()? { 201 + return Err(io::Error::new( 202 + io::ErrorKind::Other, 203 + "Specified input path does not exist", 204 + )); 205 + } 206 + inputs.push(path); 207 + } 208 + 209 + // Fallback to stdin/stdout if we're missing files 210 + let cmprss_input = match inputs.is_empty() { 211 + true => { 212 + if !std::io::stdin().is_terminal() { 213 + CmprssInput::Pipe(std::io::stdin()) 214 + } else { 215 + return Err(io::Error::new(io::ErrorKind::Other, "No specified input")); 216 + } 217 + } 218 + false => CmprssInput::Path(inputs), 158 219 }; 159 - if args.compress { 160 - compressor.compress(input, output)?; 161 - } else if args.extract { 162 - compressor.extract(input, output)?; 163 - } else { 164 - // Neither compress nor extract is specified. 165 - // Compress by default, warn if if looks like an archive. 166 - match &input { 167 - CmprssInput::Path(paths) => { 168 - for x in paths { 169 - if let Some(ext) = x.extension() { 170 - if ext == compressor.extension() { 171 - return cmprss_error( 172 - &format!("error: input appears to already be a {} archive, exiting. Use '--compress' if needed.", compressor.name())); 173 - } 174 - } 175 - } 176 - compressor.compress(input, output)?; 220 + let cmprss_output = match output { 221 + Some(path) => CmprssOutput::Path(path.to_path_buf()), 222 + None => { 223 + if !std::io::stdout().is_terminal() { 224 + CmprssOutput::Pipe(std::io::stdout()) 225 + } else { 226 + // TODO: add fallback checks 227 + // There are a number of combinations where we can't be sure 228 + println!("error: No valid output detected. This may be because the output file already exists."); 229 + return Err(io::Error::new(io::ErrorKind::Other, "No specified output")); 177 230 } 178 - _ => compressor.compress(input, output)?, 179 231 } 180 - } 232 + }; 233 + 234 + Ok(Job { 235 + input: cmprss_input, 236 + output: cmprss_output, 237 + action, 238 + }) 239 + } 240 + 241 + fn command<T: Compressor>(compressor: T, args: &CommonArgs) -> Result<(), io::Error> { 242 + let job = get_job(args)?; 243 + 244 + match job.action { 245 + Action::Compress => compressor.compress(job.input, job.output)?, 246 + Action::Extract => compressor.extract(job.input, job.output)?, 247 + }; 248 + 181 249 Ok(()) 182 250 } 183 251 184 - fn parse_gzip(args: GzipArgs) -> gzip::Gzip { 252 + fn parse_gzip(args: &GzipArgs) -> gzip::Gzip { 185 253 gzip::Gzip { 186 254 compression_level: args.compression, 187 - common_args: args.common_args.into_common(), 188 255 } 189 256 } 190 257 191 - fn parse_tar(args: TarArgs) -> tar::Tar { 192 - tar::Tar { 193 - common_args: args.common_args.into_common(), 194 - } 258 + fn parse_tar(_args: &TarArgs) -> tar::Tar { 259 + tar::Tar {} 195 260 } 196 261 197 262 fn main() -> Result<(), io::Error> { 198 263 let args = CmprssArgs::parse(); 199 264 match args.format { 200 - Some(Format::Tar(a)) => command(parse_tar(a)), 265 + Some(Format::Tar(a)) => command(parse_tar(&a), &a.common_args), 201 266 //Some(Format::Extract(a)) => command_extract(a), 202 - Some(Format::Gzip(a)) => command(parse_gzip(a)), 267 + Some(Format::Gzip(a)) => command(parse_gzip(&a), &a.common_args), 203 268 _ => Err(io::Error::new(io::ErrorKind::Other, "unknown input")), 204 269 } 205 270 }
+14 -14
src/tar.rs
··· 8 8 use crate::utils::*; 9 9 10 10 #[derive(Default)] 11 - pub struct Tar { 12 - pub common_args: CmprssCommonArgs, 13 - } 11 + pub struct Tar {} 14 12 15 13 impl Compressor for Tar { 16 14 /// Full name for tar, also used for extension ··· 18 16 "tar" 19 17 } 20 18 21 - fn common_args(&self) -> &CmprssCommonArgs { 22 - &self.common_args 23 - } 24 - 25 19 /// Tar extraction needs to specify the directory, so use the current directory 26 20 fn default_extracted_filename(&self, _in_path: &Path) -> String { 27 21 ".".to_string() ··· 42 36 if paths.len() > 1 { 43 37 return cmprss_error("only 1 archive can be extracted at a time"); 44 38 } 45 - self.extract_internal(Archive::new(File::open(paths[0])?), output) 39 + self.extract_internal(Archive::new(File::open(paths[0].as_path())?), output) 46 40 } 47 41 CmprssInput::Pipe(pipe) => self.extract_internal(Archive::new(pipe), output), 48 42 } ··· 62 56 } 63 57 CmprssOutput::Path(path) => path, 64 58 }; 59 + if !out_path.is_dir() { 60 + return cmprss_error("error: tar can only extract to a directory"); 61 + } 65 62 archive.unpack(out_path) 66 63 } 67 64 ··· 79 76 }; 80 77 for in_file in input_files { 81 78 if in_file.is_file() { 82 - archive.append_file(in_file.file_name().unwrap(), &mut File::open(in_file)?)?; 79 + archive.append_file( 80 + in_file.file_name().unwrap(), 81 + &mut File::open(in_file.as_path())?, 82 + )?; 83 83 } else if in_file.is_dir() { 84 - archive.append_dir_all(in_file.file_name().unwrap(), in_file)?; 84 + archive.append_dir_all(in_file.file_name().unwrap(), in_file.as_path())?; 85 85 } else { 86 86 return Err(io::Error::new( 87 87 io::ErrorKind::InvalidInput, ··· 112 112 113 113 // Roundtrip compress/extract 114 114 compressor.compress( 115 - CmprssInput::Path(vec![file.path()]), 116 - CmprssOutput::Path(archive.path()), 115 + CmprssInput::Path(vec![file.path().to_path_buf()]), 116 + CmprssOutput::Path(archive.path().to_path_buf()), 117 117 )?; 118 118 archive.assert(predicate::path::is_file()); 119 119 compressor.extract( 120 - CmprssInput::Path(vec![archive.path()]), 121 - CmprssOutput::Path(working_dir.path()), 120 + CmprssInput::Path(vec![archive.path().to_path_buf()]), 121 + CmprssOutput::Path(working_dir.path().to_path_buf()), 122 122 )?; 123 123 124 124 // Assert the files are identical
+7 -18
src/utils.rs
··· 1 1 use std::io; 2 - use std::path::Path; 3 - 4 - #[derive(Default)] 5 - pub struct CmprssCommonArgs { 6 - pub compress: bool, 7 - pub extract: bool, 8 - pub input: Option<String>, 9 - pub output: Option<String>, 10 - } 2 + use std::path::{Path, PathBuf}; 11 3 12 4 /// Common interface for all compressor implementations 13 5 #[allow(unused_variables)] ··· 34 26 in_path.file_stem().unwrap().to_str().unwrap().to_string() 35 27 } 36 28 37 - /// Getter method for the common arguments 38 - // TODO: There is probably a cleaner way to do this? 39 - fn common_args(&self) -> &CmprssCommonArgs; 40 - 41 29 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 42 30 cmprss_error("compress_target unimplemented") 43 31 } ··· 52 40 } 53 41 54 42 /// Defines the possible inputs of a compressor 55 - // TODO: Implement fmt for CmprssInput/CmprssOutput 56 - pub enum CmprssInput<'a> { 43 + #[derive(Debug)] 44 + pub enum CmprssInput { 57 45 /// Path(s) to the input files. 58 - Path(Vec<&'a Path>), 46 + Path(Vec<PathBuf>), 59 47 /// Input pipe 60 48 Pipe(std::io::Stdin), 61 49 } 62 50 63 51 /// Defines the possible outputs of a compressor 64 - pub enum CmprssOutput<'a> { 65 - Path(&'a Path), 52 + #[derive(Debug)] 53 + pub enum CmprssOutput { 54 + Path(PathBuf), 66 55 Pipe(std::io::Stdout), 67 56 }
+4 -2
tests/cli.rs
··· 3 3 use predicates::prelude::*; 4 4 use std::process::Command; 5 5 6 - // FIXME: Test fails because cmprss expects stdin to have data 7 6 #[allow(dead_code)] 8 - //#[test] 7 + #[test] 9 8 fn tar_roundtrip() -> Result<(), Box<dyn std::error::Error>> { 10 9 let file = assert_fs::NamedTempFile::new("test.txt")?; 11 10 file.write_str("garbage data for testing")?; ··· 21 20 let mut extract = Command::cargo_bin("cmprss")?; 22 21 extract 23 22 .arg("tar") 23 + .arg("--extract") 24 + .arg("--input") 24 25 .arg(archive.path()) 26 + .arg("--output") 25 27 .arg(working_dir.path()); 26 28 extract.assert().success(); 27 29