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 = "null-value", value_name = "VAL")]
81 pub null_value: Vec<String>,
82
83 #[arg(long = "compression", value_enum)]
86 pub compression: Option<CompressionFormat>,
87
88 #[arg(long = "debug", action)]
90 pub debug: bool,
91
92 #[arg(long = "hive", action)]
94 pub hive: bool,
95
96 #[arg(long = "single-spine-schema", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
98 pub single_spine_schema: Option<bool>,
99
100 #[arg(long = "parse-dates", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
102 pub parse_dates: Option<bool>,
103
104 #[arg(long = "decompress-in-memory", default_missing_value = "true", num_args = 0..=1, value_parser = clap::value_parser!(bool))]
106 pub decompress_in_memory: Option<bool>,
107
108 #[arg(long = "temp-dir", value_name = "DIR")]
110 pub temp_dir: Option<std::path::PathBuf>,
111
112 #[arg(long = "sheet", value_name = "SHEET")]
114 pub excel_sheet: Option<String>,
115
116 #[arg(long = "clear-cache", action)]
118 pub clear_cache: bool,
119
120 #[arg(long = "template")]
122 pub template: Option<String>,
123
124 #[arg(long = "remove-templates", action)]
126 pub remove_templates: bool,
127
128 #[arg(long = "sampling-threshold", value_name = "N")]
132 pub sampling_threshold: Option<usize>,
133
134 #[arg(long = "polars-streaming", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
136 pub polars_streaming: Option<bool>,
137
138 #[arg(long = "workaround-pivot-date-index", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
140 pub workaround_pivot_date_index: Option<bool>,
141
142 #[arg(long = "pages-lookahead")]
145 pub pages_lookahead: Option<usize>,
146
147 #[arg(long = "pages-lookback")]
150 pub pages_lookback: Option<usize>,
151
152 #[arg(long = "row-numbers", action)]
154 pub row_numbers: bool,
155
156 #[arg(long = "row-start-index")]
158 pub row_start_index: Option<usize>,
159
160 #[arg(long = "column-colors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
162 pub column_colors: Option<bool>,
163
164 #[arg(long = "generate-config", action)]
166 pub generate_config: bool,
167
168 #[arg(long = "force", requires = "generate_config", action)]
170 pub force: bool,
171
172 #[arg(long = "s3-endpoint-url", value_name = "URL")]
174 pub s3_endpoint_url: Option<String>,
175
176 #[arg(long = "s3-access-key-id", value_name = "KEY")]
178 pub s3_access_key_id: Option<String>,
179
180 #[arg(long = "s3-secret-access-key", value_name = "SECRET")]
182 pub s3_secret_access_key: Option<String>,
183
184 #[arg(long = "s3-region", value_name = "REGION")]
186 pub s3_region: Option<String>,
187}
188
189fn escape_table_cell(s: &str) -> String {
191 s.replace('|', "\\|").replace(['\n', '\r'], " ")
192}
193
194pub fn render_options_markdown() -> String {
199 let mut cmd = Args::command();
200 cmd.build();
201
202 let mut out = String::from("# Command Line Options\n\n");
203
204 out.push_str("## Usage\n\n```\n");
205 let usage = cmd.render_usage();
206 out.push_str(&usage.to_string());
207 out.push_str("\n```\n\n");
208
209 out.push_str("## Options\n\n");
210 out.push_str("| Option | Description |\n");
211 out.push_str("|--------|-------------|\n");
212
213 for arg in cmd.get_arguments() {
214 let id = arg.get_id().as_ref().to_string();
215 if id == "help" || id == "version" {
216 continue;
217 }
218
219 let option_str = if arg.is_positional() {
220 let placeholder: String = arg
221 .get_value_names()
222 .map(|names| {
223 names
224 .iter()
225 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
226 .collect::<Vec<_>>()
227 .join(" ")
228 })
229 .unwrap_or_default();
230 if arg.is_required_set() {
231 placeholder
232 } else {
233 format!("[{placeholder}]")
234 }
235 } else {
236 let mut parts = Vec::new();
237 if let Some(s) = arg.get_short() {
238 parts.push(format!("-{s}"));
239 }
240 if let Some(l) = arg.get_long() {
241 parts.push(format!("--{l}"));
242 }
243 let op = parts.join(", ");
244 let takes_val = arg.get_action().takes_values();
245 let placeholder: String = if takes_val {
246 arg.get_value_names()
247 .map(|names| {
248 names
249 .iter()
250 .map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
251 .collect::<Vec<_>>()
252 .join(" ")
253 })
254 .unwrap_or_default()
255 } else {
256 String::new()
257 };
258 if placeholder.is_empty() {
259 op
260 } else {
261 format!("{op} {placeholder}")
262 }
263 };
264
265 let help = arg
266 .get_help()
267 .map(|h| escape_table_cell(&h.to_string()))
268 .unwrap_or_else(|| "-".to_string());
269
270 out.push_str(&format!("| `{option_str}` | {help} |\n"));
271 }
272
273 out
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_compression_detection() {
282 assert_eq!(
283 CompressionFormat::from_extension(Path::new("file.csv.gz")),
284 Some(CompressionFormat::Gzip)
285 );
286 assert_eq!(
287 CompressionFormat::from_extension(Path::new("file.csv.zst")),
288 Some(CompressionFormat::Zstd)
289 );
290 assert_eq!(
291 CompressionFormat::from_extension(Path::new("file.csv.bz2")),
292 Some(CompressionFormat::Bzip2)
293 );
294 assert_eq!(
295 CompressionFormat::from_extension(Path::new("file.csv.xz")),
296 Some(CompressionFormat::Xz)
297 );
298 assert_eq!(
299 CompressionFormat::from_extension(Path::new("file.csv")),
300 None
301 );
302 assert_eq!(CompressionFormat::from_extension(Path::new("file")), None);
303 }
304
305 #[test]
306 fn test_compression_extension() {
307 assert_eq!(CompressionFormat::Gzip.extension(), "gz");
308 assert_eq!(CompressionFormat::Zstd.extension(), "zst");
309 assert_eq!(CompressionFormat::Bzip2.extension(), "bz2");
310 assert_eq!(CompressionFormat::Xz.extension(), "xz");
311 }
312}