diff --git a/cli/src/args.rs b/cli/src/args.rs index f2f51eeeb..878fe35d4 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -421,9 +421,13 @@ Positional entries: /* Output flags */ b"--stdout" => { - if output_path.is_some() { + if let Some(output_path) = output_path.take() { + return Err(Self::exit_arg_invalid(&format!( + "--stdout provided along with output file {output_path:?}" + ))); + } else if append_to_output_path { return Err(Self::exit_arg_invalid( - "--stdout provided along with output file", + "--stdout provided along with --append", )); } else if !args.is_empty() || !positional_paths.is_empty() { return Err(Self::exit_arg_invalid("--stdout provided after entries")); @@ -447,8 +451,13 @@ Positional entries: } } b"-o" | b"--output-file" => { - if output_path.is_some() { - return Err(Self::exit_arg_invalid("--output-file provided twice")); + let new_path = argv.pop_front().map(PathBuf::from).ok_or_else(|| { + Self::exit_arg_invalid("no argument provided for -o/--output-file") + })?; + if let Some(prev_path) = output_path.take() { + return Err(Self::exit_arg_invalid(&format!( + "--output-file provided twice: {prev_path:?} and {new_path:?}" + ))); } else if allow_stdout { return Err(Self::exit_arg_invalid( "--stdout provided along with output file", @@ -458,16 +467,7 @@ Positional entries: "-o/--output-file provided after entries", )); } else { - match argv.pop_front() { - Some(path) => { - output_path = Some(path.into()); - } - None => { - return Err(Self::exit_arg_invalid( - "no argument provided for -o/--output-file", - )); - } - } + output_path = Some(new_path); } } @@ -684,14 +684,27 @@ pub mod extract { #[derive(Debug)] pub enum OutputCollation { ConcatenateStdout, - Filesystem { output_dir: PathBuf, mkdir: bool }, + Filesystem { + output_dir: Option, + mkdir: bool, + }, + } + + #[derive(Debug)] + pub enum InputType { + StreamingStdin, + ZipPaths(Vec), } #[derive(Debug)] pub struct Extract { - pub collation: Option, + pub output: OutputCollation, pub args: Vec, - pub positional_zips: Vec, + pub input: InputType, + } + + impl Extract { + const PATTERN_SPEC: &'static str = "[:glob|:lit|:rx[:i]]"; } impl CommandFormat for Extract { @@ -700,17 +713,18 @@ pub mod extract { const COMMAND_DESCRIPTION: &'static str = "Extract individual entries or an entire archive into a stream or the filesystem."; - /* TODO: support reading a zip from stdin! It avoids some extraction optimizations, but we - * can use the streaming API! */ const USAGE_LINE: &'static str = "[-h|--help] [OUTPUT-FLAGS] [ENTRY-SPECS]... [--stdin|[--] ZIP-PATH...]"; fn generate_help() -> String { - " + let pattern_spec = Self::PATTERN_SPEC; + format!( + " -h, --help Print help Output flags: Where and how to collate the extracted entries. + -d, --output-directory Output directory path to write extracted entries into. Paths for extracted entries will be constructed by interpreting entry @@ -734,8 +748,32 @@ Where and how to collate the extracted entries. This will write output to stdout even if stdout is a tty. ENTRY SPECS: +After output flags are provided, entry specs are processed in order until an +input argument is reached. Attributes modify later entry patterns. + +Sticky attributes: +These flags apply to every entry pattern that comes after them until reset by +another instance of the same attribute. + + -a, --strip-components + -a, --remove-prefix{pattern_spec} + -a, --transform{pattern_spec} + -a, --add-prefix + +Non-sticky attributes: +These flags only apply to the next entry pattern after them, and may not +be repeated. + + -a, --not + +Entry patterns: ??? + -a, --type [file|dir|symlink] + -a, --max-depth + -a, --min-depth + -a, --path{pattern_spec} + -a, --name{pattern_spec} Input arguments: Zip file inputs to extract from can be specified in exactly one of two ways: @@ -758,12 +796,15 @@ Positional paths: all provided paths must exist and point to an existing zip file. Pipes are not supported and will produce an error. " - .to_string() + ) } fn parse_argv(mut argv: VecDeque) -> Result { - let mut collation: Option = None; + let mut output_dir: Option = None; + let mut mkdir_flag: bool = false; + let mut stdout_flag: bool = false; let mut args: Vec = Vec::new(); + let mut stdin_flag: bool = false; let mut positional_zips: Vec = Vec::new(); while let Some(arg) = argv.pop_front() { @@ -773,7 +814,72 @@ Positional paths: return Err(ArgParseError::StdoutMessage(help_text)); } - /* Transition to positional args */ + /* Output args */ + b"-d" | b"--output-directory" => { + let new_path = argv.pop_front().map(PathBuf::from).ok_or_else(|| { + Self::exit_arg_invalid("no argument provided for -d/--output-directory") + })?; + if let Some(prev_path) = output_dir.take() { + return Err(Self::exit_arg_invalid(&format!( + "--output-directory provided twice: {prev_path:?} and {new_path:?}" + ))); + } else if stdout_flag { + return Err(Self::exit_arg_invalid( + "--stdout provided along with output dir", + )); + } else if !args.is_empty() || stdin_flag || !positional_zips.is_empty() { + return Err(Self::exit_arg_invalid( + "-d/--output-directory provided after entry specs or inputs", + )); + } else { + output_dir = Some(new_path); + } + } + b"--mkdir" => { + if mkdir_flag { + return Err(Self::exit_arg_invalid("--mkdir provided twice")); + } else if stdout_flag { + return Err(Self::exit_arg_invalid( + "--stdout provided along with --mkdir", + )); + } else if !args.is_empty() || stdin_flag || !positional_zips.is_empty() { + return Err(Self::exit_arg_invalid( + "--mkdir provided after entry specs or inputs", + )); + } else { + mkdir_flag = true; + } + } + b"--stdout" => { + if let Some(output_dir) = output_dir.take() { + return Err(Self::exit_arg_invalid(&format!( + "--stdout provided along with output directory {output_dir:?}" + ))); + } else if stdout_flag { + return Err(Self::exit_arg_invalid("--stdout provided twice")); + } else if mkdir_flag { + return Err(Self::exit_arg_invalid( + "--stdout provided along with --mkdir", + )); + } else if !args.is_empty() || stdin_flag || !positional_zips.is_empty() { + return Err(Self::exit_arg_invalid( + "--stdout provided after entry specs or inputs", + )); + } else { + stdout_flag = true; + } + } + + /* Transition to entry specs */ + b"-a" => { + args.push(ExtractArg::Glob); + } + + /* Transition to input args */ + b"--stdin" => { + stdin_flag = true; + break; + } b"--" => break, arg_bytes => { if arg_bytes.starts_with(b"-") { @@ -789,12 +895,37 @@ Positional paths: } positional_zips.extend(argv.into_iter().map(|arg| arg.into())); + if stdin_flag && !positional_zips.is_empty() { + return Err(Self::exit_arg_invalid(&format!( + "--stdin was provided at the same time as positional args {positional_zips:?}" + ))); + } + let input = if stdin_flag { + InputType::StreamingStdin + } else { + InputType::ZipPaths(positional_zips) + }; + + let output = if stdout_flag { + OutputCollation::ConcatenateStdout + } else { + OutputCollation::Filesystem { + output_dir, + mkdir: mkdir_flag, + } + }; Ok(Self { - collation, + output, args, - positional_zips, + input, }) } } + + impl crate::driver::ExecuteCommand for Extract { + fn execute(self, err: impl std::io::Write) -> Result<(), crate::CommandError> { + crate::extract::execute_extract(err, self) + } + } } diff --git a/cli/src/extract.rs b/cli/src/extract.rs new file mode 100644 index 000000000..0655653bb --- /dev/null +++ b/cli/src/extract.rs @@ -0,0 +1,9 @@ +use std::io::{self, Write}; + +use crate::{args::extract::*, CommandError, WrapCommandErr}; + +pub fn execute_extract(mut err: impl Write, extract: Extract) -> Result<(), CommandError> { + writeln!(err, "asdf!").unwrap(); + dbg!(extract); + Ok(()) +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 11d79d070..73d1c1397 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,6 +6,7 @@ use std::{fs, io}; pub mod args; pub mod compress; +pub mod extract; pub enum ErrHandle { Output(W), @@ -159,7 +160,7 @@ pub mod driver { match command { ZipCommand::Info => todo!("info command not implemented"), - ZipCommand::Extract(_extract) => todo!("extract command not implemented"), + ZipCommand::Extract(extract) => extract.do_main(err), ZipCommand::Compress(compress) => compress.do_main(err), } }