use std::fmt::Display;
use std::fs::FileType;
use std::num::ParseIntError;
use std::process;
use chrono::{DateTime, Duration, Utc};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap::parser::ValuesRef;
use clap_complete::Shell;
use indexmap::IndexSet;
use regex::{Match, Regex};
use crate::calendar::Calendar;
use crate::error::{MyError, MyResult};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum DirKind {
Asc,
Desc,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum OrderKind {
Dir,
Group,
Name,
Ext,
Size(DirKind),
Time(DirKind),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RecentKind {
Hour,
Day,
Week,
Month,
Year,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Recent {
pub kind: RecentKind,
pub count: u32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum EntryKind {
File,
Dir,
Link,
Other,
}
pub enum ShellKind {
Bash,
PowerShell,
}
pub struct Config {
pub curr_time: DateTime<Utc>,
pub min_depth: Option<usize>,
pub max_depth: Option<usize>,
pub show_indent: bool,
pub all_files: bool,
pub order_files: Vec<OrderKind>,
pub order_name: bool,
pub filter_recent: Option<Recent>,
pub filter_type: Option<EntryKind>,
pub show_total: bool,
pub only_path: bool,
pub escape_path: bool,
pub null_path: bool,
pub abs_path: bool,
pub win_path: bool,
pub completion: Option<ShellKind>,
pub patterns: Vec<String>,
}
impl OrderKind {
fn from(ch: char) -> Option<OrderKind> {
match ch {
'n' => Some(Self::Name),
'e' => Some(Self::Ext),
's' => Some(Self::Size(DirKind::Asc)),
't' | 'd' => Some(Self::Time(DirKind::Asc)),
_ => None,
}
}
fn ascending(self: Self) -> Self {
match self {
Self::Size(_) => Self::Size(DirKind::Asc),
Self::Time(_) => Self::Time(DirKind::Asc),
any => any,
}
}
fn descending(self: Self) -> Self {
match self {
Self::Size(_) => Self::Size(DirKind::Desc),
Self::Time(_) => Self::Time(DirKind::Desc),
any => any,
}
}
}
impl RecentKind {
fn from(value: &str) -> RecentKind {
match value {
"h" => Self::Hour,
"d" => Self::Day,
"w" => Self::Week,
"m" => Self::Month,
"y" => Self::Year,
_ => Self::Hour,
}
}
}
impl Recent {
pub fn new(kind: RecentKind, count: u32) -> Recent {
Recent { kind, count }
}
pub fn subtract(&self, time: DateTime<Utc>) -> DateTime<Utc> {
match self.kind {
RecentKind::Hour => time - Duration::hours(self.count as i64),
RecentKind::Day => time - Duration::days(self.count as i64),
RecentKind::Week => time - Duration::weeks(self.count as i64),
RecentKind::Month => Calendar::from(time).subtract_month(self.count),
RecentKind::Year => Calendar::from(time).subtract_year(self.count),
}
}
}
impl EntryKind {
pub fn from(file_type: FileType) -> EntryKind {
if file_type.is_file() {
Self::File
} else if file_type.is_dir() {
Self::Dir
} else if file_type.is_symlink() {
Self::Link
} else {
Self::Other
}
}
}
impl Config {
pub fn new(name: String, args: Vec<String>) -> MyResult<Config> {
let mut command = Self::create_command(name);
let matches = Self::create_matches(&mut command, args)?;
let config = Self::create_config(&mut command, matches)?;
if let Some(completion) = config.completion {
Self::create_completion(&mut command, completion);
process::exit(1);
}
return Ok(config);
}
pub fn default() -> Config {
Config {
curr_time: Utc::now(),
min_depth: None,
max_depth: None,
show_indent: false,
all_files: false,
order_files: Vec::new(),
order_name: false,
filter_recent: None,
filter_type: None,
show_total: false,
only_path: false,
escape_path: false,
null_path: false,
abs_path: false,
win_path: false,
completion: None,
patterns: Vec::new(),
}
}
pub fn start_time(&self) -> Option<DateTime<Utc>> {
return self.filter_recent.map(|x| x.subtract(self.curr_time));
}
const RECURSE_SHORT: &'static str = "Find files in subdirectories.";
const DEPTH_SHORT: &'static str = "Find files to maximum depth M-N.";
const INDENT_SHORT: &'static str = "Indent files in subdirectories.";
const ALLFILES_SHORT: &'static str = "Show all files (including .* and __*__).";
const ORDER_SHORT: &'static str = "Sort files by order [nest][+-]...";
const RECENT_SHORT: &'static str = "Include recent files [hdwmy][N]...";
const TYPE_SHORT: &'static str = "Include files by type [fdl]...";
const TOTAL_SHORT: &'static str = "Show total file size.";
const ONLYPATH_SHORT: &'static str = "Show paths only (twice to show all attributes).";
const NULLPATH_SHORT: &'static str = "Show paths only (with null separator for xargs).";
const ABSPATH_SHORT: &'static str = "Show absolute paths.";
#[cfg(target_family = "windows")]
const WINPATH_SHORT: &'static str = "Show Windows paths.";
const SHELL_SHORT: &'static str = "Create completion script.";
const PATTERNS_SHORT: &'static str = "File matching patterns.";
const RECURSE_LONG: &'static str = "\
Find files in subdirectories.";
const DEPTH_LONG: &'static str = "\
Find files to maximum depth:
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.";
const INDENT_LONG: &'static str = "\
Indent files in subdirectories.";
const ALLFILES_LONG: &'static str = "\
Show all files:
Include Unix hidden files like \".bashrc\".
Include Python cache directories \"__pycache__\".";
const ORDER_LONG: &'static str = "\
Sort files by order:
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.";
const RECENT_LONG: &'static str = "\
Include recent files:
Use \"-rh\" to include one hour old files.
Use \"-rd\" to include one day old files.
Use \"-rw2\" to include two week old files.
Use \"-rm6\" to include six month old files.
Use \"-ry10\" to include ten year old files.";
const TYPE_LONG: &'static str = "\
Include files by type:
Use \"-tf\" to include files.
Use \"-td\" to include directories.
Use \"-tl\" to include links.";
const TOTAL_LONG: &'static str = "\
Show total file size, and number of files and directories.";
const ONLYPATH_LONG: &'static str = "\
Show paths only:
Use \"-x\" to show paths only.
Use \"-xx\" to show all attributes.
By default show all attributes when writing to the console.
By default show escaped paths when writing to a file.";
const NULLPATH_LONG: &'static str = "\
Show paths only with null separator for xargs.";
const ABSPATH_LONG: &'static str = "\
Show absolute paths.";
#[cfg(target_family = "windows")]
const WINPATH_LONG: &'static str = "\
Show Windows paths:
C:\\Path\\file.txt not /c/Path/file.txt in Bash.";
const SHELL_LONG: &'static str = "\
Create completion script:
Use \"--completion bash\" to create script for Bash.
Use \"--completion ps\" to create script for PowerShell.";
const PATTERNS_LONG: &'static str = "\
File matching patterns.";
fn create_command(name: String) -> Command {
let mut index = 0;
let command = Command::new(name)
.version(clap::crate_version!())
.about(clap::crate_description!())
.author(clap::crate_authors!());
let command = command.arg(Self::create_arg("recurse", &mut index)
.long("recurse")
.short('s')
.action(ArgAction::SetTrue)
.help(Self::RECURSE_SHORT)
.long_help(Self::RECURSE_LONG));
let command = command.arg(Self::create_arg("depth", &mut index)
.long("depth")
.short('d')
.action(ArgAction::Set)
.value_name("DEPTH")
.help(Self::DEPTH_SHORT)
.long_help(Self::DEPTH_LONG));
let command = command.arg(Self::create_arg("indent", &mut index)
.long("indent")
.short('i')
.action(ArgAction::SetTrue)
.help(Self::INDENT_SHORT)
.long_help(Self::INDENT_LONG));
let command = command.arg(Self::create_arg("allfiles", &mut index)
.long("allfiles")
.short('a')
.action(ArgAction::SetTrue)
.help(Self::ALLFILES_SHORT)
.long_help(Self::ALLFILES_LONG));
let command = command.arg(Self::create_arg("order", &mut index)
.long("order")
.short('o')
.action(ArgAction::Set)
.value_name("ORDER")
.help(Self::ORDER_SHORT)
.long_help(Self::ORDER_LONG));
let command = command.arg(Self::create_arg("recent", &mut index)
.long("recent")
.short('r')
.action(ArgAction::Set)
.value_name("RECENT")
.help(Self::RECENT_SHORT)
.long_help(Self::RECENT_LONG));
let command = command.arg(Self::create_arg("type", &mut index)
.long("type")
.short('t')
.action(ArgAction::Set)
.value_name("TYPE")
.help(Self::TYPE_SHORT)
.long_help(Self::TYPE_LONG));
let command = command.arg(Self::create_arg("total", &mut index)
.long("total")
.action(ArgAction::SetTrue)
.help(Self::TOTAL_SHORT)
.long_help(Self::TOTAL_LONG));
let command = command.arg(Self::create_arg("onlypath", &mut index)
.long("onlypath")
.short('x')
.action(ArgAction::Count)
.help(Self::ONLYPATH_SHORT)
.long_help(Self::ONLYPATH_LONG));
let command = command.arg(Self::create_arg("nullpath", &mut index)
.long("nullpath")
.short('z')
.action(ArgAction::SetTrue)
.help(Self::NULLPATH_SHORT)
.long_help(Self::NULLPATH_LONG));
let command = command.arg(Self::create_arg("abspath", &mut index)
.long("abspath")
.short('q')
.action(ArgAction::SetTrue)
.help(Self::ABSPATH_SHORT)
.long_help(Self::ABSPATH_LONG));
#[cfg(target_family = "windows")]
let command = command.arg(Self::create_arg("winpath", &mut index)
.long("winpath")
.short('w')
.action(ArgAction::SetTrue)
.help(Self::WINPATH_SHORT)
.long_help(Self::WINPATH_LONG));
let command = command.arg(Self::create_arg("completion", &mut index)
.long("completion")
.action(ArgAction::Set)
.value_name("SHELL")
.value_parser(["bash", "ps"])
.hide_possible_values(true)
.help(Self::SHELL_SHORT)
.long_help(Self::SHELL_LONG));
let command = command.arg(Self::create_arg("patterns", &mut index)
.action(ArgAction::Append)
.default_value(".")
.help(Self::PATTERNS_SHORT)
.long_help(Self::PATTERNS_LONG));
return command;
}
fn create_arg(name: &'static str, index: &mut usize) -> Arg {
*index += 1;
return Arg::new(name).display_order(*index);
}
fn create_matches(command: &mut Command, args: Vec<String>) -> clap::error::Result<ArgMatches> {
match command.try_get_matches_from_mut(args) {
Ok(found) => Ok(found),
Err(error) => match error.kind() {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
let error = error.to_string();
let error = error.trim_end();
eprintln!("{error}");
process::exit(1);
},
_ => Err(error),
}
}
}
fn create_config(command: &mut Command, matches: ArgMatches) -> MyResult<Config> {
let recurse = matches.get_flag("recurse");
let (min_depth, max_depth) = Self::parse_depth(command, matches.get_one("depth"), recurse)?;
let show_indent = matches.get_flag("indent");
let all_files = matches.get_flag("allfiles");
let (order_files, order_name) = Self::parse_order(command, matches.get_one("order"), max_depth, show_indent)?;
let filter_recent = Self::parse_recent(command, matches.get_one("recent"))?;
let filter_type = Self::parse_type(command, matches.get_one("type"))?;
let show_total = matches.get_flag("total");
let (only_path, escape_path, null_path) = Self::parse_only(matches.get_count("onlypath"), matches.get_flag("nullpath"));
let abs_path = matches.get_flag("abspath");
#[cfg(target_family = "windows")]
let win_path = matches.get_flag("winpath");
#[cfg(not(target_family = "windows"))]
let win_path = false;
let completion = Self::parse_completion(command, matches.get_one("completion"))?;
let patterns = Self::parse_values(matches.get_many("patterns"));
let config = Config {
curr_time: Utc::now(),
min_depth,
max_depth,
show_indent,
all_files,
order_files,
order_name,
filter_recent,
filter_type,
show_total,
only_path,
escape_path,
null_path,
abs_path,
win_path,
completion,
patterns,
};
return Ok(config);
}
fn parse_depth(
command: &mut Command,
value: Option<&String>,
recurse: bool,
) -> MyResult<(Option<usize>, Option<usize>)> {
if recurse {
Ok((Some(1), None))
} else if let Some(value) = value {
match value.parse::<usize>() {
Ok(value) => Ok((Some(1), Some(value + 1))),
Err(_) => {
let re = Regex::new("^(\\d+)?-(\\d+)?$")?;
match re.captures(value) {
Some(captures) => {
let min_depth = Self::parse_integer(captures.get(1))?;
let max_depth = Self::parse_integer(captures.get(2))?;
let min_depth = min_depth.map(|x| x + 1).or(Some(1));
let max_depth = max_depth.map(|x| x + 1);
Ok((min_depth, max_depth))
}
None => Err(Self::make_error(command, "depth", value)),
}
}
}
} else {
Ok((Some(1), Some(1)))
}
}
fn parse_integer(matched: Option<Match>) -> Result<Option<usize>, ParseIntError> {
match matched {
Some(m) => m.as_str().parse().map(|x| Some(x)),
None => Ok(None),
}
}
fn parse_order(
command: &mut Command,
value: Option<&String>,
max_depth: Option<usize>,
show_indent: bool,
) -> MyResult<(Vec<OrderKind>, bool)> {
let mut order_files = IndexSet::new();
let mut order_name = false;
if show_indent {
order_files.insert(OrderKind::Dir);
}
if let Some(value) = value {
let mut order = None;
for ch in value.chars() {
if ch.is_alphabetic() {
if let Some(order) = order {
order_files.insert(order);
}
order = OrderKind::from(ch);
if order == None {
return Err(Self::make_error(command, "order", value));
}
if order == Some(OrderKind::Name) {
order_name = true;
}
} else if ch == '+' {
order = order.map(|x| x.ascending());
} else if ch == '-' {
order = order.map(|x| x.descending());
} else {
return Err(Self::make_error(command, "order", value));
}
}
if let Some(order) = order {
order_files.insert(order);
}
}
if order_files.is_empty() {
if let Some(max_depth) = max_depth {
if max_depth <= 1 {
order_files.insert(OrderKind::Group);
}
}
}
order_files.insert(OrderKind::Dir);
order_files.insert(OrderKind::Name);
return Ok((order_files.into_iter().collect(), order_name));
}
fn parse_recent(command: &mut Command, value: Option<&String>) -> MyResult<Option<Recent>> {
if let Some(value) = value {
let re = Regex::new("^([hdwmy])(\\d+)?$")?;
return match re.captures(value) {
Some(captures) => {
let kind = captures.get(1)
.map(|x| x.as_str())
.map(|x| RecentKind::from(x))
.unwrap_or(RecentKind::Hour);
let count = captures.get(2)
.map(|x| x.as_str())
.map(|x| x.parse())
.unwrap_or(Ok(1))?;
Ok(Some(Recent::new(kind, count)))
}
None => Err(Self::make_error(command, "recent", value)),
}
}
return Ok(None);
}
fn parse_type(command: &mut Command, value: Option<&String>) -> MyResult<Option<EntryKind>> {
let value = value.map(String::as_ref);
match value {
Some("f") => Ok(Some(EntryKind::File)),
Some("d") => Ok(Some(EntryKind::Dir)),
Some("l") => Ok(Some(EntryKind::Link)),
Some(value) => Err(Self::make_error(command, "type", value)),
None => Ok(None),
}
}
fn parse_only(count: u8, flag: bool) -> (bool, bool, bool) {
if flag {
(true, false, true)
} else {
match count {
0 => {
let piped = atty::isnt(atty::Stream::Stdout);
(piped, piped, false)
}
1 => (true, false, false),
_ => (false, false, false),
}
}
}
fn parse_values(values: Option<ValuesRef<String>>) -> Vec<String> {
match values {
Some(values) => values.map(String::to_string).collect(),
None => vec![],
}
}
fn parse_completion(command: &mut Command, value: Option<&String>) -> MyResult<Option<ShellKind>> {
let value = value.map(String::as_ref);
match value {
Some("bash") => Ok(Some(ShellKind::Bash)),
Some("ps") => Ok(Some(ShellKind::PowerShell)),
Some(value) => Err(Self::make_error(command, "completion", value)),
None => Ok(None),
}
}
fn create_completion(command: &mut Command, value: ShellKind) {
let mut stdout = std::io::stdout();
let name = clap::crate_name!();
let value = match value {
ShellKind::Bash => Shell::Bash,
ShellKind::PowerShell => Shell::PowerShell,
};
clap_complete::generate(value, command, name, &mut stdout);
}
fn make_error<T: Display>(command: &mut Command, option: &str, value: T) -> MyError {
let message = format!("Invalid {option} option: {value}");
let error = command.error(clap::error::ErrorKind::ValueValidation, message);
return MyError::Clap(error);
}
}
#[cfg(test)]
mod tests {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_depth_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(Some(1), config.min_depth);
assert_eq!(Some(1), config.max_depth);
let args = vec!["ex", "-d4"];
let config = create_config(args);
assert_eq!(Some(1), config.min_depth);
assert_eq!(Some(5), config.max_depth);
let args = vec!["ex", "-d-4"];
let config = create_config(args);
assert_eq!(Some(1), config.min_depth);
assert_eq!(Some(5), config.max_depth);
let args = vec!["ex", "-d2-4"];
let config = create_config(args);
assert_eq!(Some(3), config.min_depth);
assert_eq!(Some(5), config.max_depth);
let args = vec!["ex", "-d2-"];
let config = create_config(args);
assert_eq!(Some(3), config.min_depth);
assert_eq!(None, config.max_depth);
let args = vec!["ex", "-s"];
let config = create_config(args);
assert_eq!(Some(1), config.min_depth);
assert_eq!(None, config.max_depth);
let args = vec!["ex", "-s", "-d4"];
let config = create_config(args);
assert_eq!(Some(1), config.min_depth);
assert_eq!(None, config.max_depth);
}
#[test]
fn test_indent_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.show_indent);
let args = vec!["ex", "-i"];
let config = create_config(args);
assert_eq!(true, config.show_indent);
let args = vec!["ex", "--indent"];
let config = create_config(args);
assert_eq!(true, config.show_indent);
}
#[test]
fn test_allfiles_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.all_files);
let args = vec!["ex", "-a"];
let config = create_config(args);
assert_eq!(true, config.all_files);
let args = vec!["ex", "--allfiles"];
let config = create_config(args);
assert_eq!(true, config.all_files);
}
#[test]
fn test_order_is_handled() {
let expected = vec![OrderKind::Group, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Name, OrderKind::Dir];
let args = vec!["ex", "-on"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(true, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-oe"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Size(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-os"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Size(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-os+"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Size(DirKind::Desc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-os-"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-ot"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-ot+"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Time(DirKind::Desc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-ot-"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-od"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-od+"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Time(DirKind::Desc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-od-"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Size(DirKind::Asc), OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "--order=est"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Size(DirKind::Asc), OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "--order=e+s+t+"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Size(DirKind::Desc), OrderKind::Time(DirKind::Desc), OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "--order=e-s-t-"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
}
#[test]
fn test_order_defaults_to_group_with_no_recursion() {
let expected = vec![OrderKind::Group, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Group, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-d0"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-d1"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-d2"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-s"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-oe"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-oe", "-d0"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-oe", "-d1"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-oe", "-d2"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-oe", "-s"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
}
#[test]
fn test_order_defaults_to_directory_with_indent() {
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-i"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-i", "-d0"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-i", "-d1"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-i", "-d2"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Name];
let args = vec!["ex", "-i", "-s"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Ext, OrderKind::Name];
let args = vec!["ex", "-i", "-oe"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Ext, OrderKind::Name];
let args = vec!["ex", "-i", "-oe", "-d0"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Ext, OrderKind::Name];
let args = vec!["ex", "-i", "-oe", "-d1"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Ext, OrderKind::Name];
let args = vec!["ex", "-i", "-oe", "-d2"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
let expected = vec![OrderKind::Dir, OrderKind::Ext, OrderKind::Name];
let args = vec!["ex", "-i", "-oe", "-s"];
let config = create_config(args);
assert_eq!(expected, config.order_files);
assert_eq!(false, config.order_name);
}
#[test]
fn test_recent_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(None, config.filter_recent);
let args = vec!["ex", "-rh"];
let config = create_config(args);
assert_eq!(Some(Recent::new(RecentKind::Hour, 1)), config.filter_recent);
let args = vec!["ex", "-rd"];
let config = create_config(args);
assert_eq!(Some(Recent::new(RecentKind::Day, 1)), config.filter_recent);
let args = vec!["ex", "-rw2"];
let config = create_config(args);
assert_eq!(Some(Recent::new(RecentKind::Week, 2)), config.filter_recent);
let args = vec!["ex", "-rm6"];
let config = create_config(args);
assert_eq!(Some(Recent::new(RecentKind::Month, 6)), config.filter_recent);
let args = vec!["ex", "--recent=y10"];
let config = create_config(args);
assert_eq!(Some(Recent::new(RecentKind::Year, 10)), config.filter_recent);
}
#[test]
fn test_hour_is_subtracted_from_time() {
let now = create_time(2023, 7, 1, 0, 0, 0);
assert_eq!(create_time(2023, 7, 1, 0, 0, 0), Recent::new(RecentKind::Hour, 0).subtract(now));
assert_eq!(create_time(2023, 6, 30, 23, 0, 0), Recent::new(RecentKind::Hour, 1).subtract(now));
assert_eq!(create_time(2023, 6, 30, 22, 0, 0), Recent::new(RecentKind::Hour, 2).subtract(now));
}
#[test]
fn test_day_is_subtracted_from_time() {
let now = create_time(2023, 7, 1, 0, 0, 0);
assert_eq!(create_time(2023, 7, 1, 0, 0, 0), Recent::new(RecentKind::Day, 0).subtract(now));
assert_eq!(create_time(2023, 6, 30, 0, 0, 0), Recent::new(RecentKind::Day, 1).subtract(now));
assert_eq!(create_time(2023, 6, 29, 0, 0, 0), Recent::new(RecentKind::Day, 2).subtract(now));
}
#[test]
fn test_week_is_subtracted_from_time() {
let now = create_time(2023, 7, 1, 0, 0, 0);
assert_eq!(create_time(2023, 7, 1, 0, 0, 0), Recent::new(RecentKind::Week, 0).subtract(now));
assert_eq!(create_time(2023, 6, 24, 0, 0, 0), Recent::new(RecentKind::Week, 1).subtract(now));
assert_eq!(create_time(2023, 6, 17, 0, 0, 0), Recent::new(RecentKind::Week, 2).subtract(now));
}
#[test]
fn test_month_is_subtracted_from_time() {
let now = create_time(2023, 3, 31, 0, 0, 0);
assert_eq!(create_time(2023, 3, 31, 0, 0, 0), Recent::new(RecentKind::Month, 0).subtract(now));
assert_eq!(create_time(2023, 3, 1, 0, 0, 0), Recent::new(RecentKind::Month, 1).subtract(now));
assert_eq!(create_time(2023, 1, 31, 0, 0, 0), Recent::new(RecentKind::Month, 2).subtract(now));
assert_eq!(create_time(2022, 12, 31, 0, 0, 0), Recent::new(RecentKind::Month, 3).subtract(now));
assert_eq!(create_time(2022, 12, 1, 0, 0, 0), Recent::new(RecentKind::Month, 4).subtract(now));
assert_eq!(create_time(2022, 10, 31, 0, 0, 0), Recent::new(RecentKind::Month, 5).subtract(now));
}
#[test]
fn test_year_is_subtracted_from_time() {
let now = create_time(2024, 2, 29, 0, 0, 0);
assert_eq!(create_time(2024, 2, 29, 0, 0, 0), Recent::new(RecentKind::Year, 0).subtract(now));
assert_eq!(create_time(2023, 3, 1, 0, 0, 0), Recent::new(RecentKind::Year, 1).subtract(now));
assert_eq!(create_time(2022, 3, 1, 0, 0, 0), Recent::new(RecentKind::Year, 2).subtract(now));
assert_eq!(create_time(2021, 3, 1, 0, 0, 0), Recent::new(RecentKind::Year, 3).subtract(now));
assert_eq!(create_time(2020, 2, 29, 0, 0, 0), Recent::new(RecentKind::Year, 4).subtract(now));
}
#[test]
fn test_type_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(None, config.filter_type);
let args = vec!["ex", "-tf"];
let config = create_config(args);
assert_eq!(Some(EntryKind::File), config.filter_type);
let args = vec!["ex", "-td"];
let config = create_config(args);
assert_eq!(Some(EntryKind::Dir), config.filter_type);
let args = vec!["ex", "--type=l"];
let config = create_config(args);
assert_eq!(Some(EntryKind::Link), config.filter_type);
}
#[test]
fn test_total_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.show_total);
let args = vec!["ex", "--total"];
let config = create_config(args);
assert_eq!(true, config.show_total);
}
#[test]
fn test_only_path_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.only_path);
assert_eq!(false, config.escape_path);
assert_eq!(false, config.null_path);
let args = vec!["ex", "-x"];
let config = create_config(args);
assert_eq!(true, config.only_path);
assert_eq!(false, config.escape_path);
assert_eq!(false, config.null_path);
let args = vec!["ex", "-x", "-x"];
let config = create_config(args);
assert_eq!(false, config.only_path);
assert_eq!(false, config.escape_path);
assert_eq!(false, config.null_path);
let args = vec!["ex", "-xx"];
let config = create_config(args);
assert_eq!(false, config.only_path);
assert_eq!(false, config.escape_path);
assert_eq!(false, config.null_path);
}
#[test]
fn test_null_path_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.only_path);
assert_eq!(false, config.escape_path);
assert_eq!(false, config.null_path);
let args = vec!["ex", "-z"];
let config = create_config(args);
assert_eq!(true, config.only_path);
assert_eq!(false, config.escape_path);
assert_eq!(true, config.null_path);
}
#[test]
fn test_abs_path_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.abs_path);
let args = vec!["ex", "-q"];
let config = create_config(args);
assert_eq!(true, config.abs_path);
let args = vec!["ex", "--abspath"];
let config = create_config(args);
assert_eq!(true, config.abs_path);
}
#[test]
#[cfg(target_family = "windows")]
fn test_win_path_is_handled() {
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(false, config.win_path);
let args = vec!["ex", "-w"];
let config = create_config(args);
assert_eq!(true, config.win_path);
let args = vec!["ex", "--winpath"];
let config = create_config(args);
assert_eq!(true, config.win_path);
}
#[test]
fn test_patterns_are_handled() {
let expected = vec!["."];
let args = vec!["ex"];
let config = create_config(args);
assert_eq!(expected, config.patterns);
let expected = vec!["file1"];
let args = vec!["ex", "file1"];
let config = create_config(args);
assert_eq!(expected, config.patterns);
let expected = vec!["file1", "file2"];
let args = vec!["ex", "file1", "file2"];
let config = create_config(args);
assert_eq!(expected, config.patterns);
}
#[test]
#[should_panic(expected = "Invalid depth option: foo")]
fn test_unexpected_depth_causes_error() {
let args = vec!["ex", "-dfoo"];
create_config(args);
}
#[test]
#[should_panic(expected = "Invalid order option: foo")]
fn test_unexpected_order_causes_error() {
let args = vec!["ex", "-ofoo"];
create_config(args);
}
#[test]
#[should_panic(expected = "Invalid recent option: foo")]
fn test_unexpected_recent_causes_error() {
let args = vec!["ex", "-rfoo"];
create_config(args);
}
#[test]
#[should_panic(expected = "Invalid type option: foo")]
fn test_unexpected_type_causes_error() {
let args = vec!["ex", "-tfoo"];
create_config(args);
}
fn create_config(args: Vec<&str>) -> Config {
let mut command = Config::create_command(String::from("ex"));
let args = args.into_iter().map(String::from).collect();
let matches = Config::create_matches(&mut command, args).unwrap_or_else(handle_error);
let config = Config::create_config(&mut command, matches).unwrap_or_else(handle_error);
return config;
}
fn create_time(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> DateTime<Utc> {
let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
let time = NaiveTime::from_hms_opt(hour, minute, second).unwrap();
return DateTime::from_naive_utc_and_offset(NaiveDateTime::new(date, time), Utc);
}
fn handle_error<T, E: Display>(err: E)-> T {
panic!("{}", err);
}
}