use clap::{CommandFactory, Parser, ValueEnum};
use std::path::Path;
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum FileFormat {
Parquet,
Csv,
Tsv,
Psv,
Json,
Jsonl,
Arrow,
Avro,
Orc,
Excel,
}
impl FileFormat {
pub fn from_path(path: &Path) -> Option<Self> {
path.extension()
.and_then(|e| e.to_str())
.and_then(Self::from_extension)
}
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"parquet" => Some(Self::Parquet),
"csv" => Some(Self::Csv),
"tsv" => Some(Self::Tsv),
"psv" => Some(Self::Psv),
"json" => Some(Self::Json),
"jsonl" | "ndjson" => Some(Self::Jsonl),
"arrow" | "ipc" | "feather" => Some(Self::Arrow),
"avro" => Some(Self::Avro),
"orc" => Some(Self::Orc),
"xls" | "xlsx" | "xlsm" | "xlsb" => Some(Self::Excel),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum CompressionFormat {
Gzip,
Zstd,
Bzip2,
Xz,
}
impl CompressionFormat {
pub fn from_extension(path: &Path) -> Option<Self> {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
match ext.to_lowercase().as_str() {
"gz" => Some(Self::Gzip),
"zst" | "zstd" => Some(Self::Zstd),
"bz2" | "bz" => Some(Self::Bzip2),
"xz" => Some(Self::Xz),
_ => None,
}
} else {
None
}
}
pub fn extension(&self) -> &'static str {
match self {
Self::Gzip => "gz",
Self::Zstd => "zst",
Self::Bzip2 => "bz2",
Self::Xz => "xz",
}
}
}
pub const NUMBER_FORMAT_VALUES: &[&str] = &[
"none",
"thousands",
"european",
"si",
"swiss",
"indian",
"underscore",
"system",
];
#[derive(Clone, Parser, Debug)]
#[command(
name = "datui",
version,
about = "Data Exploration in the Terminal",
long_about = include_str!("../long_about.txt")
)]
pub struct Args {
#[arg(num_args = 0.., value_name = "PATH")]
pub paths: Vec<std::path::PathBuf>,
#[arg(long = "skip-lines")]
pub skip_lines: Option<usize>,
#[arg(long = "skip-rows")]
pub skip_rows: Option<usize>,
#[arg(long = "skip-tail-rows", value_name = "N")]
pub skip_tail_rows: Option<usize>,
#[arg(long = "no-header")]
pub no_header: Option<bool>,
#[arg(long = "delimiter")]
pub delimiter: Option<u8>,
#[arg(long = "infer-schema-length", value_name = "N")]
pub infer_schema_length: Option<usize>,
#[arg(long = "ignore-errors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub ignore_errors: Option<bool>,
#[arg(long = "null-value", value_name = "VAL")]
pub null_value: Vec<String>,
#[arg(long = "compression", value_enum)]
pub compression: Option<CompressionFormat>,
#[arg(long = "format", value_enum)]
pub format: Option<FileFormat>,
#[arg(long = "debug", action)]
pub debug: bool,
#[arg(long = "hive", action)]
pub hive: bool,
#[arg(long = "single-spine-schema", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub single_spine_schema: Option<bool>,
#[arg(long = "parse-dates", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub parse_dates: Option<bool>,
#[arg(long = "parse-strings", value_name = "COL", num_args = 0.., default_missing_value = "")]
pub parse_strings: Vec<String>,
#[arg(long = "no-parse-strings", action)]
pub no_parse_strings: bool,
#[arg(long = "decompress-in-memory", default_missing_value = "true", num_args = 0..=1, value_parser = clap::value_parser!(bool))]
pub decompress_in_memory: Option<bool>,
#[arg(long = "temp-dir", value_name = "DIR")]
pub temp_dir: Option<std::path::PathBuf>,
#[arg(long = "sheet", value_name = "SHEET")]
pub excel_sheet: Option<String>,
#[arg(long = "clear-recents", action)]
pub clear_recents: bool,
#[arg(long = "clear-cache", action)]
pub clear_cache: bool,
#[arg(long = "template")]
pub template: Option<String>,
#[arg(long = "remove-templates", action)]
pub remove_templates: bool,
#[arg(long = "sampling-threshold", value_name = "N")]
pub sampling_threshold: Option<usize>,
#[arg(long = "polars-streaming", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub polars_streaming: Option<bool>,
#[arg(long = "workaround-pivot-date-index", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub workaround_pivot_date_index: Option<bool>,
#[arg(long = "pages-lookahead")]
pub pages_lookahead: Option<usize>,
#[arg(long = "pages-lookback")]
pub pages_lookback: Option<usize>,
#[arg(long = "row-numbers", action)]
pub row_numbers: bool,
#[arg(long = "row-start-index")]
pub row_start_index: Option<usize>,
#[arg(long = "column-colors", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub column_colors: Option<bool>,
#[arg(long = "number-format", value_name = "FORMAT", value_parser = clap::builder::PossibleValuesParser::new(NUMBER_FORMAT_VALUES))]
pub number_format: Option<String>,
#[arg(long = "align-numeric-right", value_name = "BOOL", value_parser = clap::value_parser!(bool))]
pub align_numeric_right: Option<bool>,
#[arg(long = "generate-config", action)]
pub generate_config: bool,
#[arg(long = "force", requires = "generate_config", action)]
pub force: bool,
#[arg(long = "s3-endpoint-url", value_name = "URL")]
pub s3_endpoint_url: Option<String>,
#[arg(long = "s3-access-key-id", value_name = "KEY")]
pub s3_access_key_id: Option<String>,
#[arg(long = "s3-secret-access-key", value_name = "SECRET")]
pub s3_secret_access_key: Option<String>,
#[arg(long = "s3-region", value_name = "REGION")]
pub s3_region: Option<String>,
}
fn escape_table_cell(s: &str) -> String {
s.replace('|', "\\|").replace(['\n', '\r'], " ")
}
pub fn render_options_markdown() -> String {
let mut cmd = Args::command();
cmd.build();
let mut out = String::from("# Command Line Options\n\n");
out.push_str("## Usage\n\n```\n");
let usage = cmd.render_usage();
out.push_str(&usage.to_string());
out.push_str("\n```\n\n");
out.push_str("## Options\n\n");
out.push_str("| Option | Description |\n");
out.push_str("|--------|-------------|\n");
for arg in cmd.get_arguments() {
let id = arg.get_id().as_ref().to_string();
if id == "help" || id == "version" {
continue;
}
let option_str = if arg.is_positional() {
let placeholder: String = arg
.get_value_names()
.map(|names| {
names
.iter()
.map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default();
if arg.is_required_set() {
placeholder
} else {
format!("[{placeholder}]")
}
} else {
let mut parts = Vec::new();
if let Some(s) = arg.get_short() {
parts.push(format!("-{s}"));
}
if let Some(l) = arg.get_long() {
parts.push(format!("--{l}"));
}
let op = parts.join(", ");
let takes_val = arg.get_action().takes_values();
let placeholder: String = if takes_val {
arg.get_value_names()
.map(|names| {
names
.iter()
.map(|n: &clap::builder::Str| format!("<{}>", n.as_ref() as &str))
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default()
} else {
String::new()
};
if placeholder.is_empty() {
op
} else {
format!("{op} {placeholder}")
}
};
let help = arg
.get_help()
.map(|h| escape_table_cell(&h.to_string()))
.unwrap_or_else(|| "-".to_string());
out.push_str(&format!("| `{option_str}` | {help} |\n"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compression_detection() {
assert_eq!(
CompressionFormat::from_extension(Path::new("file.csv.gz")),
Some(CompressionFormat::Gzip)
);
assert_eq!(
CompressionFormat::from_extension(Path::new("file.csv.zst")),
Some(CompressionFormat::Zstd)
);
assert_eq!(
CompressionFormat::from_extension(Path::new("file.csv.bz2")),
Some(CompressionFormat::Bzip2)
);
assert_eq!(
CompressionFormat::from_extension(Path::new("file.csv.xz")),
Some(CompressionFormat::Xz)
);
assert_eq!(
CompressionFormat::from_extension(Path::new("file.csv")),
None
);
assert_eq!(CompressionFormat::from_extension(Path::new("file")), None);
}
#[test]
fn test_compression_extension() {
assert_eq!(CompressionFormat::Gzip.extension(), "gz");
assert_eq!(CompressionFormat::Zstd.extension(), "zst");
assert_eq!(CompressionFormat::Bzip2.extension(), "bz2");
assert_eq!(CompressionFormat::Xz.extension(), "xz");
}
#[test]
fn test_file_format_from_path() {
assert_eq!(
FileFormat::from_path(Path::new("data.parquet")),
Some(FileFormat::Parquet)
);
assert_eq!(
FileFormat::from_path(Path::new("data.csv")),
Some(FileFormat::Csv)
);
assert_eq!(
FileFormat::from_path(Path::new("file.jsonl")),
Some(FileFormat::Jsonl)
);
assert_eq!(FileFormat::from_path(Path::new("noext")), None);
assert_eq!(
FileFormat::from_path(Path::new("file.NDJSON")),
Some(FileFormat::Jsonl)
);
}
}