1use clap::{CommandFactory, Parser, ValueEnum};
7use std::path::Path;
8
9#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
12pub enum FileFormat {
13 Parquet,
15 Csv,
17 Tsv,
19 Psv,
21 Json,
23 Jsonl,
25 Arrow,
27 Avro,
29 Orc,
31 Excel,
33}
34
35impl FileFormat {
36 pub fn from_path(path: &Path) -> Option<Self> {
38 path.extension()
39 .and_then(|e| e.to_str())
40 .and_then(Self::from_extension)
41 }
42
43 pub fn from_extension(ext: &str) -> Option<Self> {
45 match ext.to_lowercase().as_str() {
46 "parquet" => Some(Self::Parquet),
47 "csv" => Some(Self::Csv),
48 "tsv" => Some(Self::Tsv),
49 "psv" => Some(Self::Psv),
50 "json" => Some(Self::Json),
51 "jsonl" | "ndjson" => Some(Self::Jsonl),
52 "arrow" | "ipc" | "feather" => Some(Self::Arrow),
53 "avro" => Some(Self::Avro),
54 "orc" => Some(Self::Orc),
55 "xls" | "xlsx" | "xlsm" | "xlsb" => Some(Self::Excel),
56 _ => None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
63pub enum CompressionFormat {
64 Gzip,
66 Zstd,
68 Bzip2,
70 Xz,
72}
73
74impl CompressionFormat {
75 pub fn from_extension(path: &Path) -> Option<Self> {
77 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
78 match ext.to_lowercase().as_str() {
79 "gz" => Some(Self::Gzip),
80 "zst" | "zstd" => Some(Self::Zstd),
81 "bz2" | "bz" => Some(Self::Bzip2),
82 "xz" => Some(Self::Xz),
83 _ => None,
84 }
85 } else {
86 None
87 }
88 }
89
90 pub fn extension(&self) -> &'static str {
92 match self {
93 Self::Gzip => "gz",
94 Self::Zstd => "zst",
95 Self::Bzip2 => "bz2",
96 Self::Xz => "xz",
97 }
98 }
99}
100
101pub const NUMBER_FORMAT_VALUES: &[&str] = &[
108 "none",
109 "thousands",
110 "european",
111 "si",
112 "swiss",
113 "indian",
114 "underscore",
115 "system",
116];
117
118#[derive(Clone, Parser, Debug)]
120#[command(
121 name = "datui",
122 version,
123 about = "Data Exploration in the Terminal",
124 long_about = include_str!("../long_about.txt")
125)]
126pub struct Args {
127 #[arg(required_unless_present_any = ["generate_config", "clear_cache", "remove_templates"], num_args = 1.., value_name = "PATH")]
130 pub paths: Vec<std::path::PathBuf>,
131
132 #[arg(long = "skip-lines")]
134 pub skip_lines: Option<usize>,
135
136 #[arg(long = "skip-rows")]
138 pub skip_rows: Option<usize>,
139
140 #[arg(long = "skip-tail-rows", value_name = "N")]
142 pub skip_tail_rows: Option<usize>,
143
144 #[arg(long = "no-header")]
146 pub no_header: Option<bool>,
147
148 #[arg(long = "delimiter")]
150 pub delimiter: Option<u8>,
151
152 #[arg(long = "infer-schema-length", value_name = "N")]
154 pub infer_schema_length: Option<usize>,
155
156 #[arg(long = "ignore-errors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
158 pub ignore_errors: Option<bool>,
159
160 #[arg(long = "null-value", value_name = "VAL")]
162 pub null_value: Vec<String>,
163
164 #[arg(long = "compression", value_enum)]
167 pub compression: Option<CompressionFormat>,
168
169 #[arg(long = "format", value_enum)]
172 pub format: Option<FileFormat>,
173
174 #[arg(long = "debug", action)]
176 pub debug: bool,
177
178 #[arg(long = "hive", action)]
180 pub hive: bool,
181
182 #[arg(long = "single-spine-schema", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
184 pub single_spine_schema: Option<bool>,
185
186 #[arg(long = "parse-dates", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
188 pub parse_dates: Option<bool>,
189
190 #[arg(long = "parse-strings", value_name = "COL", num_args = 0.., default_missing_value = "")]
192 pub parse_strings: Vec<String>,
193
194 #[arg(long = "no-parse-strings", action)]
196 pub no_parse_strings: bool,
197
198 #[arg(long = "decompress-in-memory", default_missing_value = "true", num_args = 0..=1, value_parser = clap::value_parser!(bool))]
200 pub decompress_in_memory: Option<bool>,
201
202 #[arg(long = "temp-dir", value_name = "DIR")]
204 pub temp_dir: Option<std::path::PathBuf>,
205
206 #[arg(long = "sheet", value_name = "SHEET")]
208 pub excel_sheet: Option<String>,
209
210 #[arg(long = "clear-cache", action)]
212 pub clear_cache: bool,
213
214 #[arg(long = "template")]
216 pub template: Option<String>,
217
218 #[arg(long = "remove-templates", action)]
220 pub remove_templates: bool,
221
222 #[arg(long = "sampling-threshold", value_name = "N")]
226 pub sampling_threshold: Option<usize>,
227
228 #[arg(long = "polars-streaming", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
230 pub polars_streaming: Option<bool>,
231
232 #[arg(long = "workaround-pivot-date-index", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
234 pub workaround_pivot_date_index: Option<bool>,
235
236 #[arg(long = "pages-lookahead")]
239 pub pages_lookahead: Option<usize>,
240
241 #[arg(long = "pages-lookback")]
244 pub pages_lookback: Option<usize>,
245
246 #[arg(long = "row-numbers", action)]
248 pub row_numbers: bool,
249
250 #[arg(long = "row-start-index")]
252 pub row_start_index: Option<usize>,
253
254 #[arg(long = "column-colors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
256 pub column_colors: Option<bool>,
257
258 #[arg(long = "number-format", value_name = "FORMAT", value_parser = clap::builder::PossibleValuesParser::new(NUMBER_FORMAT_VALUES))]
261 pub number_format: Option<String>,
262
263 #[arg(long = "align-numeric-right", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
265 pub align_numeric_right: Option<bool>,
266
267 #[arg(long = "generate-config", action)]
269 pub generate_config: bool,
270
271 #[arg(long = "force", requires = "generate_config", action)]
273 pub force: bool,
274
275 #[arg(long = "s3-endpoint-url", value_name = "URL")]
277 pub s3_endpoint_url: Option<String>,
278
279 #[arg(long = "s3-access-key-id", value_name = "KEY")]
281 pub s3_access_key_id: Option<String>,
282
283 #[arg(long = "s3-secret-access-key", value_name = "SECRET")]
285 pub s3_secret_access_key: Option<String>,
286
287 #[arg(long = "s3-region", value_name = "REGION")]
289 pub s3_region: Option<String>,
290}
291
292fn escape_table_cell(s: &str) -> String {
294 s.replace('|', "\\|").replace(['\n', '\r'], " ")
295}
296
297pub fn render_options_markdown() -> String {
302 let mut cmd = Args::command();
303 cmd.build();
304
305 let mut out = String::from("# Command Line Options\n\n");
306
307 out.push_str("## Usage\n\n```\n");
308 let usage = cmd.render_usage();
309 out.push_str(&usage.to_string());
310 out.push_str("\n```\n\n");
311
312 out.push_str("## Options\n\n");
313 out.push_str("| Option | Description |\n");
314 out.push_str("|--------|-------------|\n");
315
316 for arg in cmd.get_arguments() {
317 let id = arg.get_id().as_ref().to_string();
318 if id == "help" || id == "version" {
319 continue;
320 }
321
322 let option_str = if arg.is_positional() {
323 let placeholder: String = arg
324 .get_value_names()
325 .map(|names| {
326 names
327 .iter()
328 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
329 .collect::<Vec<_>>()
330 .join(" ")
331 })
332 .unwrap_or_default();
333 if arg.is_required_set() {
334 placeholder
335 } else {
336 format!("[{placeholder}]")
337 }
338 } else {
339 let mut parts = Vec::new();
340 if let Some(s) = arg.get_short() {
341 parts.push(format!("-{s}"));
342 }
343 if let Some(l) = arg.get_long() {
344 parts.push(format!("--{l}"));
345 }
346 let op = parts.join(", ");
347 let takes_val = arg.get_action().takes_values();
348 let placeholder: String = if takes_val {
349 arg.get_value_names()
350 .map(|names| {
351 names
352 .iter()
353 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
354 .collect::<Vec<_>>()
355 .join(" ")
356 })
357 .unwrap_or_default()
358 } else {
359 String::new()
360 };
361 if placeholder.is_empty() {
362 op
363 } else {
364 format!("{op} {placeholder}")
365 }
366 };
367
368 let help = arg
369 .get_help()
370 .map(|h| escape_table_cell(&h.to_string()))
371 .unwrap_or_else(|| "-".to_string());
372
373 out.push_str(&format!("| `{option_str}` | {help} |\n"));
374 }
375
376 out
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_compression_detection() {
385 assert_eq!(
386 CompressionFormat::from_extension(Path::new("file.csv.gz")),
387 Some(CompressionFormat::Gzip)
388 );
389 assert_eq!(
390 CompressionFormat::from_extension(Path::new("file.csv.zst")),
391 Some(CompressionFormat::Zstd)
392 );
393 assert_eq!(
394 CompressionFormat::from_extension(Path::new("file.csv.bz2")),
395 Some(CompressionFormat::Bzip2)
396 );
397 assert_eq!(
398 CompressionFormat::from_extension(Path::new("file.csv.xz")),
399 Some(CompressionFormat::Xz)
400 );
401 assert_eq!(
402 CompressionFormat::from_extension(Path::new("file.csv")),
403 None
404 );
405 assert_eq!(CompressionFormat::from_extension(Path::new("file")), None);
406 }
407
408 #[test]
409 fn test_compression_extension() {
410 assert_eq!(CompressionFormat::Gzip.extension(), "gz");
411 assert_eq!(CompressionFormat::Zstd.extension(), "zst");
412 assert_eq!(CompressionFormat::Bzip2.extension(), "bz2");
413 assert_eq!(CompressionFormat::Xz.extension(), "xz");
414 }
415
416 #[test]
417 fn test_file_format_from_path() {
418 assert_eq!(
419 FileFormat::from_path(Path::new("data.parquet")),
420 Some(FileFormat::Parquet)
421 );
422 assert_eq!(
423 FileFormat::from_path(Path::new("data.csv")),
424 Some(FileFormat::Csv)
425 );
426 assert_eq!(
427 FileFormat::from_path(Path::new("file.jsonl")),
428 Some(FileFormat::Jsonl)
429 );
430 assert_eq!(FileFormat::from_path(Path::new("noext")), None);
431 assert_eq!(
432 FileFormat::from_path(Path::new("file.NDJSON")),
433 Some(FileFormat::Jsonl)
434 );
435 }
436}