ex-cli 1.20.0

Command line tool to find, filter, sort and list files.
Documentation
use crate::cli::depth::RecurseDepth;
use crate::cli::file::FileSet;
use crate::cli::order::{OrderKind, OrderVec};
use crate::cli::recent::RecentKind;
use crate::git::flags::GitFlags;
use chrono::{DateTime, Utc};
use clap::{ArgAction, Parser};
use clap_builder::ColorChoice;
use clap_complete::Shell;
use indexmap::IndexSet;

#[derive(Default, Parser)]
#[command(name = "ex", version, about, author)]
pub struct Cli {
    /// Find files in subdirectories
    #[arg(long = "recurse", short = 's')]
    pub recurse_all: bool,

    /// Find files to maximum depth M-N
    ///
    /// Use "-d4" or "-d-4" to find files up to depth 4
    /// Use "-d2-4" to find files at depth 2, 3 or 4
    /// Use "-d2-" to find files at depth 2 and beyond
    #[arg(
        long = "depth",
        short = 'd',
        value_name = "DEPTH",
        value_parser = RecurseDepth::from_str,
        verbatim_doc_comment,
    )]
    pub recurse_depth: Option<RecurseDepth>,

    /// Indent files in subdirectories
    #[arg(long = "indent", short = 'i')]
    pub show_indent: bool,

    /// Show .* and __*__ files (twice to recurse)
    ///
    /// Use "-a" to show hidden files and directories
    /// Use "-aa" to recurse into hidden directories
    /// Include Unix hidden files like ".bashrc"
    /// Include Python cache directories "__pycache__"
    #[arg(
        long = "all-files",
        short = 'a',
        action = ArgAction::Count,
        verbatim_doc_comment,
    )]
    pub show_hidden: u8,

    /// Expand zip files
    ///
    /// Show contents of *.zip, *.7z, *.tar, *.tar.gz
    #[arg(long = "zip", short = 'z')]
    pub zip_expand: bool,

    /// Use zip password
    ///
    /// Use with caution; may be stored in shell command history
    #[arg(long = "password", value_name = "PASSWORD")]
    pub zip_password: Option<String>,

    /// Force case sensitive match
    ///
    /// Use this option to override default Windows filename matching
    #[arg(long = "case", overrides_with = "case_insensitive")]
    pub case_sensitive: bool,

    /// Force case insensitive match
    ///
    /// Use this option to override default Linux filename matching
    #[arg(long = "no-case")]
    pub case_insensitive: bool,

    /// Sort files by order [dnest][+-]...
    ///
    /// Use "-od" to sort files by directory
    /// Use "-on" to sort files by filename
    /// Use "-oe" to sort files by extension
    /// Use "-os" to sort files by size (increasing)
    /// Use "-os-" to sort files by size (decreasing)
    /// Use "-ot" to sort files by time (increasing)
    /// Use "-ot-" to sort files by time (decreasing)
    /// Use "-oest" to sort files by extension then size then time
    #[arg(
        long = "order",
        short = 'o',
        value_name = "ORDER",
        value_parser = OrderVec::from_str,
        verbatim_doc_comment,
    )]
    pub sort_order: Option<OrderVec>,

    /// Include recent files [ymwdHMS][N]...
    ///
    /// Use "-ry10" to include files up to ten years old
    /// Use "-rm6" to include files up to six months old
    /// Use "-rw2" to include files up to two weeks old
    /// Use "-rd" to include files up to one day old
    /// Use "-rH" to include files up to one hour old
    /// Use "-rM5" to include files up to five minutes old
    /// Use "-rS10" to include files up to ten seconds old
    #[arg(
        long = "recent",
        short = 'r',
        value_name = "RECENT",
        value_parser = RecentKind::from_str,
        verbatim_doc_comment,
    )]
    pub filter_recent: Option<RecentKind>,

    /// Include files by type [fedl]...
    ///
    /// Use "-tf" to include files
    /// Use "-te" to include executables
    /// Use "-td" to include directories
    /// Use "-tl" to include links
    #[arg(
        long = "type",
        short = 't',
        value_name = "TYPE",
        value_parser = FileSet::from_str,
        verbatim_doc_comment,
    )]
    pub filter_types: Option<FileSet>,

    /// Include files by Git status [xcamrui]...
    ///
    /// Use "-gx" to include all files (with untracked and ignored)
    /// Use "-gc" to include cached files (not untracked or ignored)
    /// Use "-ga" to include only added files
    /// Use "-gm" to include only modified files
    /// Use "-gr" to include only renamed files
    /// Use "-gu" to include only untracked files
    /// Use "-gi" to include only ignored files (according to .gitignore)
    #[arg(
        long = "git",
        short = 'g',
        value_name = "GIT",
        value_parser = GitFlags::from_str,
        verbatim_doc_comment,
    )]
    pub filter_git: Option<GitFlags>,

    /// Show debug information
    #[cfg(debug_assertions)]
    #[arg(long = "debug")]
    pub show_debug: bool,

    /// Show precise file size and time
    #[arg(long = "precise", short = 'p')]
    pub show_precise: bool,

    /// Show UTC file time
    #[arg(long = "utc", short = 'u')]
    pub show_utc: bool,

    /// Show total file size and number of files and directories
    #[arg(long = "total")]
    pub show_total: bool,

    /// Show file owner
    ///
    /// Show user and group names on Linux
    #[cfg(unix)]
    #[arg(long = "owner")]
    pub show_owner: bool,

    /// Show file signature
    ///
    /// The first four bytes can be used to identify some file types
    #[arg(long = "sig")]
    pub show_sig: bool,

    /// Show paths only (repeat to show all attributes)
    ///
    /// Use "-x" to show paths only
    /// Use "-xx" to show all attributes (pretty printing enabled)
    /// Use "-xxx" to show all attributes (pretty printing disabled)
    /// By default show all attributes when writing to the console
    /// By default show escaped paths when writing to a file
    #[arg(
        long = "only-path",
        short = 'x',
        action = ArgAction::Count,
        verbatim_doc_comment,
    )]
    pub only_path: u8,

    /// Show paths only (with null separator for xargs)
    #[arg(long = "null-path")]
    pub null_path: bool,

    /// Show absolute paths
    #[arg(long = "abs-path", short = 'q')]
    pub abs_path: bool,

    /// Show Windows paths
    ///
    /// For example "C:\\Path\\file.txt" not "/c/Path/file.txt" in Git Bash
    #[cfg(windows)]
    #[arg(long = "win-path", short = 'w')]
    pub win_path: bool,

    /// Show Windows versions
    ///
    /// Applies to *.exe and *.dll only
    #[cfg(windows)]
    #[arg(long = "win-ver", short = 'v')]
    pub win_ver: bool,

    /// Set current time for readme examples
    ///
    /// For example "2024-01-01T00:00:00Z"
    #[cfg(debug_assertions)]
    #[arg(long = "now", value_name = "TIME")]
    pub curr_time: Option<DateTime<Utc>>,

    /// Force terminal output for readme examples
    ///
    /// Disables piped output
    #[cfg(debug_assertions)]
    #[arg(long = "terminal")]
    pub terminal: bool,

    /// Show colored output (default "auto").
    ///
    /// Use "--color auto" to show color for terminal output only
    /// Use "--color always" to always show color
    /// Use "--color never" to never show color
    #[arg(
        long = "color",
        value_name = "COLOR",
        hide_possible_values = true,
        verbatim_doc_comment,
    )]
    pub color: Option<ColorChoice>,

    /// Hide non-fatal (e.g. permission denied) errors
    #[arg(long = "quiet")]
    pub quiet: bool,

    /// Create completion script
    ///
    /// Use "--completion bash" to create script for Bash
    /// Use "--completion elvish" to create script for Elvish
    /// Use "--completion fish" to create script for Fish
    /// Use "--completion powershell" to create script for PowerShell
    /// Use "--completion zsh" to create script for Zsh
    #[arg(
        long = "completion",
        value_name = "SHELL",
        hide_possible_values = true,
        verbatim_doc_comment,
    )]
    pub completion: Option<Shell>,

    /// File matching patterns
    #[arg(default_value = ".")]
    pub patterns: Vec<String>,
}

impl Cli {
    #[cfg(debug_assertions)]
    pub fn curr_time<G: Fn() -> DateTime<Utc>>(&self, factory: G) -> DateTime<Utc> {
        self.curr_time.unwrap_or_else(factory)
    }

    #[cfg(debug_assertions)]
    pub fn terminal(&self) -> bool {
        self.terminal
    }

    #[cfg(not(debug_assertions))]
    pub fn curr_time<G: Fn() -> DateTime<Utc>>(&self, factory: G) -> DateTime<Utc> {
        factory()
    }

    #[cfg(not(debug_assertions))]
    pub fn terminal(&self) -> bool {
        false
    }

    pub fn min_depth(&self) -> Option<usize> {
        if self.recurse_all {
            Some(1)
        } else if let Some(depth) = &self.recurse_depth {
            depth.min_depth
        } else {
            Some(1)
        }
    }

    pub fn max_depth(&self) -> Option<usize> {
        if self.recurse_all {
            None
        } else if let Some(depth) = &self.recurse_depth {
            depth.max_depth
        } else {
            Some(1)
        }
    }

    pub fn sort_order(&mut self) -> (Vec<OrderKind>, bool) {
        let mut sort_order = IndexSet::new();
        if self.show_indent {
            sort_order.insert(OrderKind::Dir);
        }
        if let Some(order) = &self.sort_order {
            for order in &order.inner {
                sort_order.insert(*order);
            }
        }
        let sort_name = sort_order.contains(&OrderKind::Name);
        if sort_order.is_empty() {
            if let Some(max_depth) = self.max_depth() {
                if max_depth <= 1 {
                    sort_order.insert(OrderKind::Group);
                }
            }
        }
        sort_order.insert(OrderKind::Dir);
        sort_order.insert(OrderKind::Name);
        let sort_order = sort_order.into_iter().collect();
        (sort_order, sort_name)
    }
}