use std::ffi::OsString;
use clap::{Error, ValueEnum, arg, builder::PossibleValue, value_parser};
use crate::{
fs::filter::{SortCase, SortField},
output::{file_name::Absolute, time::TimeFormat},
};
const SORT_FIELDS_HELP: &str = "[default: name] [possible values:
name, Name, .name, .Name, ext, ext, created,
date, age, accessed, changed,
size, inode, type, none]";
const TIME_FIELDS_HELP: &str = "[possible values:
mod|modified, acc|accessed, ch|changed, cr|created]";
const FORMAT_STYLE_FIELDS_HELP: &str = "[possible values:
default, iso, long-iso, full-iso, relative, \"+<CUSTOM_FORMAT>\"]";
pub fn get_command() -> clap::Command {
clap::Command::new(clap::crate_name!())
.author(clap::crate_authors!())
.about(clap::crate_description!())
.version(include_str!(concat!(env!("OUT_DIR"), "/version_string.txt")))
.disable_help_flag(true)
.disable_version_flag(true)
.args_override_self(true)
.arg(arg!([FILE]...).value_parser(clap::value_parser!(OsString)).hide_short_help(true))
.next_help_heading("META OPTIONS")
.arg(arg!(--stdin "read file names from stdin"))
.arg(arg!(-'?' --help "Print help").action(clap::ArgAction::HelpShort))
.arg(arg!(-v --version "Print help").action(clap::ArgAction::Version))
.next_help_heading("LAYOUT OPTIONS")
.arg(arg!(-'1' --oneline "display one entry per line"))
.arg(arg!(-l --long "display extended file metadata as a table"))
.arg(arg!(-G --grid "display entries as a grid (default)"))
.arg(arg!(-x --across "sort the grid across, rather than downwards"))
.arg(arg!(-R --recurse "recurse into directories"))
.arg(arg!(-T --tree "recurse into directories as a tree"))
.arg(arg!(-L --level <DEPTH> "limit the depth of recursion")
.value_parser(value_parser!(usize)))
.arg(arg!(--code <MODE> "summarise lines of code by language, recursing the tree or git repo")
.num_args(0..=1)
.require_equals(true)
.value_parser(value_parser!(CodeContent))
.default_missing_value("both")
.hide_possible_values(true))
.arg(arg!(--"follow-symlinks" "drill down into symbolic links that point to directories"))
.arg(arg!(-w --width <COLS> "set screen width in columns")
.value_parser(value_parser!(usize)))
.next_help_heading("DISPLAY OPTIONS")
.arg(arg!(-F --classify <WHEN> "display type indicator by file names")
.num_args(0..=1)
.value_parser(value_parser!(ShowWhen))
.default_missing_value("auto"))
.arg(arg!(-X --dereference "dereference symbolic links when displaying information"))
.arg(arg!(--absolute "display entries with their absolute path")
.num_args(0..=1)
.action(clap::ArgAction::Set)
.value_parser(value_parser!(Absolute))
.default_missing_value("on")
.default_value("off")
.hide_default_value(true))
.arg(arg!(--color <WHEN> "When to use colours.")
.alias("colour")
.num_args(0..=1)
.value_parser(value_parser!(ShowWhen))
.default_missing_value("auto")
.default_value("auto"))
.arg(arg!(--"color-scale" <FIELDS> "highlight value of FIELDS distinctly")
.num_args(0..)
.value_parser(value_parser!(ColorScaleArgs))
.default_missing_value("all")
.value_delimiter(','))
.arg(arg!(--"color-scale-mode" <MODE> "mode for --color-scale")
.num_args(1)
.value_parser(value_parser!(ColorScaleModeArgs))
.default_value("gradient"))
.arg(arg!(--icons <WHEN> "when to display icons")
.num_args(0..=1)
.value_parser(value_parser!(ShowWhen))
.default_missing_value("auto"))
.arg(arg!(--hyperlink <WHEN> "when to display entries as hyperlinks")
.num_args(0..=1)
.value_parser(value_parser!(ShowWhen))
.default_missing_value("auto"))
.arg(arg!(--"no-quotes" "don't quote file names with spaces"))
.arg(arg!(--"short-nix" "abbreviate Nix store hashes in file names and paths"))
.next_help_heading("FILTERING OPTIONS")
.arg(arg!(-a --all... "show hidden files. Use this twice to also show the '.' and '..' directories"))
.arg(arg!(-A --"almost-all" "equivalent to --all; included for compatibility with `ls -A`"))
.arg(arg!(-d --"treat-dirs-as-files" "treat directories as files; don't list their contents")
.alias("list-dirs") .conflicts_with_all(["recurse", "tree"]))
.arg(arg!(-D --"only-dirs" "list only directories"))
.arg(arg!(-f --"only-files" "list only files"))
.arg(arg!(--"show-symlinks" "explicitly show symbolic links (with --only-dirs and --only-files)"))
.arg(arg!(--"no-symlinks" "do not show symbolic links"))
.arg(arg!(-I --"ignore-glob" <GLOBS> "glob patterns (pipe-separated) of files to ignore"))
.arg(arg!(--"git-ignore" "ignore files mentioned in '.gitignore'"))
.next_help_heading("SORTING OPTIONS")
.arg(arg!(--"group-directories-first" "list directories before other files").id("dirs-first"))
.arg(arg!(--"group-directories-last" "list directories after other files").id("dirs-last"))
.arg(arg!(-s --sort <FIELD>)
.help(format!("which field to sort by {SORT_FIELDS_HELP}"))
.value_parser(value_parser!(SortField))
.default_value("name")
.hide_default_value(true)
.hide_possible_values(true))
.arg(arg!(-r --reverse "reverse the sort order"))
.next_help_heading("LONG VIEW OPTIONS")
.arg(arg!(-h --header "add a header row to each column"))
.arg(arg!(-i --inode "list each file's inode number"))
.arg(arg!(--loc <MODE> "add lines-of-code and language columns [modes: lines, percent, both]")
.num_args(0..=1)
.require_equals(true)
.value_parser(value_parser!(CodeContent))
.default_missing_value("both")
.hide_possible_values(true))
.arg(arg!(-o --"octal-permissions" "list each file's permission in octal format"))
.arg(arg!(-H --links "list each file's number of hard links"))
.arg(arg!(-b --binary "show file sizes with binary prefixes"))
.arg(arg!(-B --bytes "show file sizes in bytes, without any prefixes"))
.arg(arg!(--"total-size" "show the size of a directory as the one of its content (unix only)"))
.arg(arg!(-S --blocksize "list size of allocated file system blocks"))
.arg(arg!(-g --group "list each file's group"))
.arg(arg!(--"smart-group" "only show group if it has a different name from owner"))
.arg(arg!(-n --numeric "show user and group as their numeric IDs"))
.arg(arg!(-t --time <FIELD>).help(format!("which timestamp field to show {TIME_FIELDS_HELP}"))
.value_parser(value_parser!(TimeArgs))
.conflicts_with_all(["modified", "accessed", "changed", "created"])
.hide_possible_values(true))
.arg(arg!(-m --modified "show the modified timestamp field (replace default field, combinable)"))
.arg(arg!(-u --accessed "show the accessed timestamp field (replace default field, combinable)"))
.arg(arg!(--changed "show the changed timestamp field (replace default field, combinable)"))
.arg(arg!(-U --created "show the created timestamp field (replace default field, combinable)"))
.arg(arg!(--"time-style" <STYLE>)
.help(format!("how to format timestamps {FORMAT_STYLE_FIELDS_HELP}"))
.value_parser(TimeFormatParser)
.hide_possible_values(false))
.arg(arg!(-O --flags "list file flags (Mac, BSD, and Windows only)").id("file-flags"))
.arg(arg!(-Z --context "list each file's security context").id("security-context"))
.arg(arg!(--git "list each file's Git status, if tracked or ignored"))
.arg(arg!(--"git-repos" "list root of git-tree status"))
.arg(arg!(--"git-repos-no-status" "list each git-repos branch name (much faster)"))
.arg(arg!(-M --mounts "show mount details (Linux and macOS only)"))
.arg(arg!(-'@' --extended "list each file's extended attributes and sizes"))
.arg(arg!(--"no-permissions" "suppress the permissions field"))
.arg(arg!(--"no-filesize" "suppress the filesize field"))
.arg(arg!(--"no-user" "suppress the user field"))
.arg(arg!(--"no-time" "suppress the time field"))
.arg(arg!(--"no-git" "suppress Git fields (overrides --git, --git-repos, --git-repos-no-status)"))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ShowWhen {
Always,
Auto,
Never,
}
impl ValueEnum for ShowWhen {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Always, Self::Auto, Self::Never]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(
match self {
Self::Always => "always",
Self::Auto => "auto",
Self::Never => "never",
}
.into(),
)
}
fn from_str(s: &str, _ignore_case: bool) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"" | "auto" | "automatic" => Ok(Self::Auto),
"always" => Ok(Self::Always),
"never" => Ok(Self::Never),
e => Err(String::from(e)),
}
}
}
#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
pub enum ColorScaleArgs {
All,
Age,
Size,
}
#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
pub enum ColorScaleModeArgs {
Fixed,
Gradient,
}
#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
pub enum CodeContent {
Lines,
Percent,
Both,
}
impl ValueEnum for SortField {
fn value_variants<'a>() -> &'a [Self] {
&[
Self::Name(SortCase::AaBbCc),
Self::Name(SortCase::ABCabc),
Self::NameMixHidden(SortCase::AaBbCc),
Self::NameMixHidden(SortCase::ABCabc),
Self::Size,
Self::Extension(SortCase::AaBbCc),
Self::Extension(SortCase::ABCabc),
Self::ModifiedDate,
Self::ModifiedAge,
Self::ChangedDate,
Self::AccessedDate,
Self::CreatedDate,
#[cfg(unix)]
Self::FileInode,
Self::FileType,
Self::Unsorted,
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Self::Name(SortCase::AaBbCc) => PossibleValue::new("name").alias("filename"),
Self::Name(SortCase::ABCabc) => PossibleValue::new("Name").alias("Filename"),
Self::NameMixHidden(SortCase::AaBbCc) => PossibleValue::new(".name").alias(".filename"),
Self::NameMixHidden(SortCase::ABCabc) => PossibleValue::new(".Name").alias(".Filename"),
Self::Size => PossibleValue::new("size"),
Self::Extension(SortCase::AaBbCc) => PossibleValue::new("ext").alias("extension"),
Self::Extension(SortCase::ABCabc) => PossibleValue::new("Ext").alias("Extension"),
Self::ModifiedDate => {
PossibleValue::new("date").aliases(vec!["time", "mod", "modified", "new", "newest"])
}
Self::ModifiedAge => PossibleValue::new("age").aliases(vec!["old", "oldest"]),
Self::ChangedDate => PossibleValue::new("changed").alias("ch"),
Self::AccessedDate => PossibleValue::new("accessed").alias("acc"),
Self::CreatedDate => PossibleValue::new("created").alias("cr"),
#[cfg(unix)]
Self::FileInode => PossibleValue::new("inode"),
Self::FileType => PossibleValue::new("type"),
Self::Unsorted => PossibleValue::new("none"),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TimeArgs {
Modified,
Changed,
Accessed,
Created,
}
impl ValueEnum for TimeArgs {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Modified, Self::Changed, Self::Accessed, Self::Created]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
Self::Modified => PossibleValue::new("modified").alias("mod"),
Self::Changed => PossibleValue::new("changed").alias("ch"),
Self::Accessed => PossibleValue::new("accessed").alias("acc"),
Self::Created => PossibleValue::new("created").alias("cr"),
})
}
}
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct TimeFormatParser;
impl clap::builder::ValueParserFactory for TimeFormat {
type Parser = TimeFormatParser;
fn value_parser() -> Self::Parser {
TimeFormatParser
}
}
impl clap::builder::TypedValueParser for TimeFormatParser {
type Value = TimeFormat;
fn parse_ref(
&self,
cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, Error> {
match TimeFormat::try_from_str(value.to_str().unwrap()) {
Err(s) => Err(Error::raw(clap::error::ErrorKind::InvalidValue, s).with_cmd(cmd)),
Ok(v) => Ok(v),
}
}
}
impl ValueEnum for Absolute {
fn value_variants<'a>() -> &'a [Self] {
&[Self::On, Self::Off, Self::Follow]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
Self::On => PossibleValue::new("on").alias("yes"),
Self::Off => PossibleValue::new("off").alias("no"),
Self::Follow => PossibleValue::new("follow"),
})
}
}
#[cfg(test)]
pub mod test {
use super::*;
pub fn mock_cli<I, T>(itr: I) -> clap::ArgMatches
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
get_command().no_binary_name(true).get_matches_from(itr)
}
pub fn mock_cli_try<I, T>(itr: I) -> Result<clap::ArgMatches, clap::error::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
get_command().no_binary_name(true).try_get_matches_from(itr)
}
#[test]
fn deduce_files() {
let cli = mock_cli(vec!["file1", "file2"]);
assert_eq!(
cli.get_many("FILE")
.unwrap()
.map(OsString::as_os_str)
.collect::<Vec<_>>(),
["file1", "file2"]
);
}
}