1use clap::{Parser, Subcommand};
4use std::path::{Path, PathBuf};
5
6#[derive(Parser)]
7#[command(
8 name = "zzz",
9 version,
10 about = "zzz: compression multitool",
11 long_about = "Create and extract archives in multiple formats (zst, tgz, txz, zip, 7z) with smart file filtering and magic number detection"
12)]
13pub struct Cli {
14 #[command(subcommand)]
15 pub command: Commands,
16
17 #[arg(short, long, global = true)]
19 pub verbose: bool,
20
21 #[arg(short = 'j', long, global = true, default_value = "0")]
23 pub threads: u32,
24}
25
26#[derive(Subcommand)]
27pub enum Commands {
28 #[command(alias = "c")]
30 Compress {
31 #[arg(short, long, default_value = "19", value_parser = clap::value_parser!(i32).range(1..=22))]
33 level: i32,
34
35 #[arg(short, long)]
37 output: Option<PathBuf>,
38
39 #[arg(short = 'P', long)]
41 progress: bool,
42
43 #[arg(short = 'e', long)]
45 exclude: Vec<String>,
46
47 #[arg(long)]
49 keep_xattrs: bool,
50
51 #[arg(long)]
53 keep_permissions: bool,
54
55 #[arg(long)]
57 keep_ownership: bool,
58
59 #[arg(long)]
61 follow_symlinks: bool,
62
63 #[arg(long)]
65 allow_symlink_escape: bool,
66
67 #[arg(long)]
69 redact: bool,
70
71 #[arg(long)]
73 strip_timestamps: bool,
74
75 #[arg(short = 'E', long)]
77 no_default_excludes: bool,
78
79 #[arg(short = 'f', long, value_parser = parse_format)]
81 format: Option<crate::formats::Format>,
82
83 #[arg(short = 'y', long)]
85 overwrite: bool,
86
87 input: PathBuf,
89
90 #[arg(short = 'p', long)]
92 password: Option<String>,
93 },
94
95 #[command(alias = "x")]
97 Extract {
98 archive: PathBuf,
100
101 destination: Option<PathBuf>,
103
104 #[arg(short = 'C', long)]
106 directory: Option<PathBuf>,
107
108 #[arg(short = 'P', long)]
110 progress: bool,
111
112 #[arg(long, default_value = "0")]
114 strip_components: usize,
115
116 #[arg(long)]
118 keep_xattrs: bool,
119
120 #[arg(long)]
122 strip_timestamps: bool,
123
124 #[arg(long)]
126 keep_permissions: bool,
127
128 #[arg(long)]
130 keep_ownership: bool,
131
132 #[arg(short = 'y', long)]
134 overwrite: bool,
135
136 #[arg(short = 'p', long)]
138 password: Option<String>,
139 },
140
141 #[command(alias = "l")]
143 List {
144 archive: PathBuf,
146 },
147
148 #[command(alias = "t")]
150 Test {
151 archive: PathBuf,
153 },
154}
155
156fn parse_format(s: &str) -> Result<crate::formats::Format, String> {
158 match s.to_lowercase().as_str() {
159 "zst" | "zstd" => Ok(crate::formats::Format::Zstd),
160 "tgz" | "gz" | "gzip" => Ok(crate::formats::Format::Gzip),
161 "txz" | "xz" => Ok(crate::formats::Format::Xz),
162 "zip" => Ok(crate::formats::Format::Zip),
163 "7z" | "sevenz" => Ok(crate::formats::Format::SevenZ),
164 _ => Err(format!(
165 "unsupported format '{s}'. Supported formats: zst, tgz, txz, zip, 7z"
166 )),
167 }
168}
169
170impl Cli {
171 pub fn get_output_path(
173 input: &Path,
174 output: Option<PathBuf>,
175 format: Option<crate::formats::Format>,
176 ) -> PathBuf {
177 output.unwrap_or_else(|| {
178 let mut path = input.to_path_buf();
179 let extension = match format {
180 Some(f) => f.extension(),
181 None => "zst",
182 };
183
184 if let Some(filename) = path.file_name() {
185 let mut new_filename = filename.to_os_string();
186 new_filename.push(".");
187 new_filename.push(extension);
188 path.set_file_name(new_filename);
189 } else {
190 path.set_extension(extension);
191 }
192 path
193 })
194 }
195
196 pub fn get_extract_dir(destination: Option<PathBuf>, directory: Option<PathBuf>) -> PathBuf {
198 directory
199 .or(destination)
200 .unwrap_or_else(|| PathBuf::from("."))
201 }
202}