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(num_args = 0.., value_name = "PATH")]
131 pub paths: Vec<std::path::PathBuf>,
132
133 #[arg(long = "skip-lines")]
135 pub skip_lines: Option<usize>,
136
137 #[arg(long = "skip-rows")]
139 pub skip_rows: Option<usize>,
140
141 #[arg(long = "skip-tail-rows", value_name = "N")]
143 pub skip_tail_rows: Option<usize>,
144
145 #[arg(long = "no-header")]
147 pub no_header: Option<bool>,
148
149 #[arg(long = "delimiter")]
151 pub delimiter: Option<u8>,
152
153 #[arg(long = "infer-schema-length", value_name = "N")]
155 pub infer_schema_length: Option<usize>,
156
157 #[arg(long = "ignore-errors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
159 pub ignore_errors: Option<bool>,
160
161 #[arg(long = "null-value", value_name = "VAL")]
163 pub null_value: Vec<String>,
164
165 #[arg(long = "compression", value_enum)]
168 pub compression: Option<CompressionFormat>,
169
170 #[arg(long = "format", value_enum)]
173 pub format: Option<FileFormat>,
174
175 #[arg(long = "debug", action)]
177 pub debug: bool,
178
179 #[arg(long = "hive", action)]
181 pub hive: bool,
182
183 #[arg(long = "single-spine-schema", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
185 pub single_spine_schema: Option<bool>,
186
187 #[arg(long = "parse-dates", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
189 pub parse_dates: Option<bool>,
190
191 #[arg(long = "parse-strings", value_name = "COL", num_args = 0.., default_missing_value = "")]
193 pub parse_strings: Vec<String>,
194
195 #[arg(long = "no-parse-strings", action)]
197 pub no_parse_strings: bool,
198
199 #[arg(long = "decompress-in-memory", default_missing_value = "true", num_args = 0..=1, value_parser = clap::value_parser!(bool))]
201 pub decompress_in_memory: Option<bool>,
202
203 #[arg(long = "temp-dir", value_name = "DIR")]
205 pub temp_dir: Option<std::path::PathBuf>,
206
207 #[arg(long = "sheet", value_name = "SHEET")]
209 pub excel_sheet: Option<String>,
210
211 #[arg(long = "clear-recents", action)]
213 pub clear_recents: bool,
214
215 #[arg(long = "clear-cache", action)]
217 pub clear_cache: bool,
218
219 #[arg(long = "template")]
221 pub template: Option<String>,
222
223 #[arg(long = "remove-templates", action)]
225 pub remove_templates: bool,
226
227 #[arg(long = "sampling-threshold", value_name = "N")]
231 pub sampling_threshold: Option<usize>,
232
233 #[arg(long = "polars-streaming", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
235 pub polars_streaming: Option<bool>,
236
237 #[arg(long = "workaround-pivot-date-index", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
239 pub workaround_pivot_date_index: Option<bool>,
240
241 #[arg(long = "pages-lookahead")]
244 pub pages_lookahead: Option<usize>,
245
246 #[arg(long = "pages-lookback")]
249 pub pages_lookback: Option<usize>,
250
251 #[arg(long = "row-numbers", action)]
253 pub row_numbers: bool,
254
255 #[arg(long = "row-start-index")]
257 pub row_start_index: Option<usize>,
258
259 #[arg(long = "column-colors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
261 pub column_colors: Option<bool>,
262
263 #[arg(long = "number-format", value_name = "FORMAT", value_parser = clap::builder::PossibleValuesParser::new(NUMBER_FORMAT_VALUES))]
266 pub number_format: Option<String>,
267
268 #[arg(long = "align-numeric-right", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
270 pub align_numeric_right: Option<bool>,
271
272 #[arg(long = "generate-config", action)]
274 pub generate_config: bool,
275
276 #[arg(long = "force", requires = "generate_config", action)]
278 pub force: bool,
279
280 #[arg(long = "s3-endpoint-url", value_name = "URL")]
282 pub s3_endpoint_url: Option<String>,
283
284 #[arg(long = "s3-access-key-id", value_name = "KEY")]
286 pub s3_access_key_id: Option<String>,
287
288 #[arg(long = "s3-secret-access-key", value_name = "SECRET")]
290 pub s3_secret_access_key: Option<String>,
291
292 #[arg(long = "s3-region", value_name = "REGION")]
294 pub s3_region: Option<String>,
295}
296
297fn escape_table_cell(s: &str) -> String {
299 s.replace('|', "\\|").replace(['\n', '\r'], " ")
300}
301
302pub fn render_options_markdown() -> String {
307 let mut cmd = Args::command();
308 cmd.build();
309
310 let mut out = String::from("# Command Line Options\n\n");
311
312 out.push_str("## Usage\n\n```\n");
313 let usage = cmd.render_usage();
314 out.push_str(&usage.to_string());
315 out.push_str("\n```\n\n");
316
317 out.push_str("## Options\n\n");
318 out.push_str("| Option | Description |\n");
319 out.push_str("|--------|-------------|\n");
320
321 for arg in cmd.get_arguments() {
322 let id = arg.get_id().as_ref().to_string();
323 if id == "help" || id == "version" {
324 continue;
325 }
326
327 let option_str = if arg.is_positional() {
328 let placeholder: String = arg
329 .get_value_names()
330 .map(|names| {
331 names
332 .iter()
333 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
334 .collect::<Vec<_>>()
335 .join(" ")
336 })
337 .unwrap_or_default();
338 if arg.is_required_set() {
339 placeholder
340 } else {
341 format!("[{placeholder}]")
342 }
343 } else {
344 let mut parts = Vec::new();
345 if let Some(s) = arg.get_short() {
346 parts.push(format!("-{s}"));
347 }
348 if let Some(l) = arg.get_long() {
349 parts.push(format!("--{l}"));
350 }
351 let op = parts.join(", ");
352 let takes_val = arg.get_action().takes_values();
353 let placeholder: String = if takes_val {
354 arg.get_value_names()
355 .map(|names| {
356 names
357 .iter()
358 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
359 .collect::<Vec<_>>()
360 .join(" ")
361 })
362 .unwrap_or_default()
363 } else {
364 String::new()
365 };
366 if placeholder.is_empty() {
367 op
368 } else {
369 format!("{op} {placeholder}")
370 }
371 };
372
373 let help = arg
374 .get_help()
375 .map(|h| escape_table_cell(&h.to_string()))
376 .unwrap_or_else(|| "-".to_string());
377
378 out.push_str(&format!("| `{option_str}` | {help} |\n"));
379 }
380
381 out
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_compression_detection() {
390 assert_eq!(
391 CompressionFormat::from_extension(Path::new("file.csv.gz")),
392 Some(CompressionFormat::Gzip)
393 );
394 assert_eq!(
395 CompressionFormat::from_extension(Path::new("file.csv.zst")),
396 Some(CompressionFormat::Zstd)
397 );
398 assert_eq!(
399 CompressionFormat::from_extension(Path::new("file.csv.bz2")),
400 Some(CompressionFormat::Bzip2)
401 );
402 assert_eq!(
403 CompressionFormat::from_extension(Path::new("file.csv.xz")),
404 Some(CompressionFormat::Xz)
405 );
406 assert_eq!(
407 CompressionFormat::from_extension(Path::new("file.csv")),
408 None
409 );
410 assert_eq!(CompressionFormat::from_extension(Path::new("file")), None);
411 }
412
413 #[test]
414 fn test_compression_extension() {
415 assert_eq!(CompressionFormat::Gzip.extension(), "gz");
416 assert_eq!(CompressionFormat::Zstd.extension(), "zst");
417 assert_eq!(CompressionFormat::Bzip2.extension(), "bz2");
418 assert_eq!(CompressionFormat::Xz.extension(), "xz");
419 }
420
421 #[test]
422 fn test_file_format_from_path() {
423 assert_eq!(
424 FileFormat::from_path(Path::new("data.parquet")),
425 Some(FileFormat::Parquet)
426 );
427 assert_eq!(
428 FileFormat::from_path(Path::new("data.csv")),
429 Some(FileFormat::Csv)
430 );
431 assert_eq!(
432 FileFormat::from_path(Path::new("file.jsonl")),
433 Some(FileFormat::Jsonl)
434 );
435 assert_eq!(FileFormat::from_path(Path::new("noext")), None);
436 assert_eq!(
437 FileFormat::from_path(Path::new("file.NDJSON")),
438 Some(FileFormat::Jsonl)
439 );
440 }
441}