1use clap::{CommandFactory, Parser, ValueEnum};
7use std::path::Path;
8
9#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
11pub enum CompressionFormat {
12 Gzip,
14 Zstd,
16 Bzip2,
18 Xz,
20}
21
22impl CompressionFormat {
23 pub fn from_extension(path: &Path) -> Option<Self> {
25 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
26 match ext.to_lowercase().as_str() {
27 "gz" => Some(Self::Gzip),
28 "zst" | "zstd" => Some(Self::Zstd),
29 "bz2" | "bz" => Some(Self::Bzip2),
30 "xz" => Some(Self::Xz),
31 _ => None,
32 }
33 } else {
34 None
35 }
36 }
37
38 pub fn extension(&self) -> &'static str {
40 match self {
41 Self::Gzip => "gz",
42 Self::Zstd => "zst",
43 Self::Bzip2 => "bz2",
44 Self::Xz => "xz",
45 }
46 }
47}
48
49#[derive(Parser, Debug)]
51#[command(
52 name = "datui",
53 version,
54 about = "Data Exploration in the Terminal",
55 long_about = include_str!("../long_about.txt")
56)]
57pub struct Args {
58 #[arg(required_unless_present_any = ["generate_config", "clear_cache", "remove_templates"], num_args = 1.., value_name = "PATH")]
61 pub paths: Vec<std::path::PathBuf>,
62
63 #[arg(long = "skip-lines")]
65 pub skip_lines: Option<usize>,
66
67 #[arg(long = "skip-rows")]
69 pub skip_rows: Option<usize>,
70
71 #[arg(long = "no-header")]
73 pub no_header: Option<bool>,
74
75 #[arg(long = "delimiter")]
77 pub delimiter: Option<u8>,
78
79 #[arg(long = "compression", value_enum)]
82 pub compression: Option<CompressionFormat>,
83
84 #[arg(long = "debug", action)]
86 pub debug: bool,
87
88 #[arg(long = "hive", action)]
90 pub hive: bool,
91
92 #[arg(long = "single-spine-schema", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
94 pub single_spine_schema: Option<bool>,
95
96 #[arg(long = "parse-dates", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
98 pub parse_dates: Option<bool>,
99
100 #[arg(long = "decompress-in-memory", default_missing_value = "true", num_args = 0..=1, value_parser = clap::value_parser!(bool))]
102 pub decompress_in_memory: Option<bool>,
103
104 #[arg(long = "temp-dir", value_name = "DIR")]
106 pub temp_dir: Option<std::path::PathBuf>,
107
108 #[arg(long = "sheet", value_name = "SHEET")]
110 pub excel_sheet: Option<String>,
111
112 #[arg(long = "clear-cache", action)]
114 pub clear_cache: bool,
115
116 #[arg(long = "template")]
118 pub template: Option<String>,
119
120 #[arg(long = "remove-templates", action)]
122 pub remove_templates: bool,
123
124 #[arg(long = "sampling-threshold", value_name = "N")]
128 pub sampling_threshold: Option<usize>,
129
130 #[arg(long = "polars-streaming", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
132 pub polars_streaming: Option<bool>,
133
134 #[arg(long = "workaround-pivot-date-index", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
136 pub workaround_pivot_date_index: Option<bool>,
137
138 #[arg(long = "pages-lookahead")]
141 pub pages_lookahead: Option<usize>,
142
143 #[arg(long = "pages-lookback")]
146 pub pages_lookback: Option<usize>,
147
148 #[arg(long = "row-numbers", action)]
150 pub row_numbers: bool,
151
152 #[arg(long = "row-start-index")]
154 pub row_start_index: Option<usize>,
155
156 #[arg(long = "column-colors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
158 pub column_colors: Option<bool>,
159
160 #[arg(long = "generate-config", action)]
162 pub generate_config: bool,
163
164 #[arg(long = "force", requires = "generate_config", action)]
166 pub force: bool,
167
168 #[arg(long = "s3-endpoint-url", value_name = "URL")]
170 pub s3_endpoint_url: Option<String>,
171
172 #[arg(long = "s3-access-key-id", value_name = "KEY")]
174 pub s3_access_key_id: Option<String>,
175
176 #[arg(long = "s3-secret-access-key", value_name = "SECRET")]
178 pub s3_secret_access_key: Option<String>,
179
180 #[arg(long = "s3-region", value_name = "REGION")]
182 pub s3_region: Option<String>,
183}
184
185fn escape_table_cell(s: &str) -> String {
187 s.replace('|', "\\|").replace(['\n', '\r'], " ")
188}
189
190pub fn render_options_markdown() -> String {
195 let mut cmd = Args::command();
196 cmd.build();
197
198 let mut out = String::from("# Command Line Options\n\n");
199
200 out.push_str("## Usage\n\n```\n");
201 let usage = cmd.render_usage();
202 out.push_str(&usage.to_string());
203 out.push_str("\n```\n\n");
204
205 out.push_str("## Options\n\n");
206 out.push_str("| Option | Description |\n");
207 out.push_str("|--------|-------------|\n");
208
209 for arg in cmd.get_arguments() {
210 let id = arg.get_id().as_ref().to_string();
211 if id == "help" || id == "version" {
212 continue;
213 }
214
215 let option_str = if arg.is_positional() {
216 let placeholder: String = arg
217 .get_value_names()
218 .map(|names| {
219 names
220 .iter()
221 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
222 .collect::<Vec<_>>()
223 .join(" ")
224 })
225 .unwrap_or_default();
226 if arg.is_required_set() {
227 placeholder
228 } else {
229 format!("[{placeholder}]")
230 }
231 } else {
232 let mut parts = Vec::new();
233 if let Some(s) = arg.get_short() {
234 parts.push(format!("-{s}"));
235 }
236 if let Some(l) = arg.get_long() {
237 parts.push(format!("--{l}"));
238 }
239 let op = parts.join(", ");
240 let takes_val = arg.get_action().takes_values();
241 let placeholder: String = if takes_val {
242 arg.get_value_names()
243 .map(|names| {
244 names
245 .iter()
246 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
247 .collect::<Vec<_>>()
248 .join(" ")
249 })
250 .unwrap_or_default()
251 } else {
252 String::new()
253 };
254 if placeholder.is_empty() {
255 op
256 } else {
257 format!("{op} {placeholder}")
258 }
259 };
260
261 let help = arg
262 .get_help()
263 .map(|h| escape_table_cell(&h.to_string()))
264 .unwrap_or_else(|| "-".to_string());
265
266 out.push_str(&format!("| `{option_str}` | {help} |\n"));
267 }
268
269 out
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_compression_detection() {
278 assert_eq!(
279 CompressionFormat::from_extension(Path::new("file.csv.gz")),
280 Some(CompressionFormat::Gzip)
281 );
282 assert_eq!(
283 CompressionFormat::from_extension(Path::new("file.csv.zst")),
284 Some(CompressionFormat::Zstd)
285 );
286 assert_eq!(
287 CompressionFormat::from_extension(Path::new("file.csv.bz2")),
288 Some(CompressionFormat::Bzip2)
289 );
290 assert_eq!(
291 CompressionFormat::from_extension(Path::new("file.csv.xz")),
292 Some(CompressionFormat::Xz)
293 );
294 assert_eq!(
295 CompressionFormat::from_extension(Path::new("file.csv")),
296 None
297 );
298 assert_eq!(CompressionFormat::from_extension(Path::new("file")), None);
299 }
300
301 #[test]
302 fn test_compression_extension() {
303 assert_eq!(CompressionFormat::Gzip.extension(), "gz");
304 assert_eq!(CompressionFormat::Zstd.extension(), "zst");
305 assert_eq!(CompressionFormat::Bzip2.extension(), "bz2");
306 assert_eq!(CompressionFormat::Xz.extension(), "xz");
307 }
308}