use crate::fs::feature::xattr;
use crate::options::{flags, OptionsError, NumberSource, Vars};
use crate::options::parser::MatchedFlags;
use crate::output::{View, Mode, TerminalWidth, grid, details};
use crate::output::grid_details::{self, RowThreshold};
use crate::output::file_name::Options as FileStyle;
use crate::output::table::{Column, TimeType, SizeFormat, UserFormat, Options as TableOptions};
use crate::output::time::TimeFormat;
impl View {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let mode = Mode::deduce(matches, vars)?;
let width = TerminalWidth::deduce(matches, vars)?;
let file_style = FileStyle::deduce(matches, vars)?;
Ok(Self { mode, width, file_style })
}
}
impl Mode {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let long_count = matches.count(flags::LONG);
let has_columns = matches.get(flags::COLUMNS).is_some()
|| matches.get(flags::FORMAT).is_some();
let long = long_count > 0 || has_columns;
let tree = matches.has(flags::TREE);
let grid = matches.has(flags::GRID);
let oneline = matches.has(flags::ONE_LINE);
if long && tree {
let details = details::Options::deduce_long(matches, vars, long_count)?;
return Ok(Self::Details(details));
}
if long && grid {
let details = details::Options::deduce_long(matches, vars, long_count)?;
let grid = grid::Options::deduce(matches);
let row_threshold = RowThreshold::deduce(vars)?;
let grid_details = grid_details::Options { grid, details, row_threshold };
return Ok(Self::GridDetails(grid_details));
}
if long {
let details = details::Options::deduce_long(matches, vars, long_count)?;
return Ok(Self::Details(details));
}
if tree {
let details = details::Options::deduce_tree(matches);
return Ok(Self::Details(details));
}
if oneline {
return Ok(Self::Lines);
}
let grid = grid::Options::deduce(matches);
Ok(Self::Grid(grid))
}
}
impl grid::Options {
fn deduce(matches: &MatchedFlags) -> Self {
Self {
across: matches.has(flags::ACROSS),
}
}
}
impl details::Options {
fn deduce_tree(matches: &MatchedFlags) -> Self {
Self {
table: None,
header: false,
xattr: xattr::ENABLED && matches.has(flags::EXTENDED),
}
}
fn deduce_long<V: Vars>(matches: &MatchedFlags, vars: &V, long_count: u8) -> Result<Self, OptionsError> {
Ok(Self {
table: Some(TableOptions::deduce(matches, vars, long_count)?),
header: matches.has(flags::HEADER) || long_count >= 3,
xattr: xattr::ENABLED && matches.has(flags::EXTENDED),
})
}
}
impl TerminalWidth {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars;
if let Some(w) = matches.get_usize(flags::WIDTH) {
return Ok(Self::Set(w));
}
if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) {
match columns.parse() {
Ok(width) => {
Ok(Self::Set(width))
}
Err(e) => {
let source = NumberSource::Env(vars::COLUMNS);
Err(OptionsError::FailedParse(columns, source, e))
}
}
}
else {
Ok(Self::Automatic)
}
}
}
impl RowThreshold {
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars;
if let Some(columns) = vars.get(vars::LX_GRID_ROWS).and_then(|s| s.into_string().ok()) {
match columns.parse() {
Ok(rows) => {
Ok(Self::MinimumRows(rows))
}
Err(e) => {
let source = NumberSource::Env(vars::LX_GRID_ROWS);
Err(OptionsError::FailedParse(columns, source, e))
}
}
}
else {
Ok(Self::AlwaysGrid)
}
}
}
impl TableOptions {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V, long_count: u8) -> Result<Self, OptionsError> {
let time_format = TimeFormat::deduce(matches, vars)?;
let size_format = SizeFormat::deduce(matches);
let user_format = UserFormat::deduce(matches);
let columns = deduce_columns(matches, long_count);
let total_size = matches.has(flags::TOTAL_SIZE);
Ok(Self { size_format, time_format, user_format, columns, total_size })
}
}
fn format_columns(name: &str) -> Option<Vec<Column>> {
if let Some(ref cfg) = *crate::config::CONFIG
&& let Some(columns) = cfg.format.get(name) {
let cols: Vec<Column> = columns.iter()
.filter_map(|s| Column::from_name(s))
.collect();
if !cols.is_empty() {
return Some(cols);
}
}
let cols = match name {
"long" => vec![
Column::Permissions,
Column::FileSize,
#[cfg(unix)]
Column::User,
Column::Timestamp(TimeType::Modified),
],
"long2" => vec![
Column::Permissions,
Column::FileSize,
#[cfg(unix)]
Column::User,
#[cfg(unix)]
Column::Group,
Column::Timestamp(TimeType::Modified),
Column::VcsStatus,
],
"long3" => vec![
Column::Permissions,
#[cfg(unix)]
Column::HardLinks,
Column::FileSize,
#[cfg(unix)]
Column::Blocks,
#[cfg(unix)]
Column::User,
#[cfg(unix)]
Column::Group,
Column::Timestamp(TimeType::Modified),
Column::Timestamp(TimeType::Changed),
Column::Timestamp(TimeType::Created),
Column::Timestamp(TimeType::Accessed),
Column::VcsStatus,
],
_ => return None,
};
Some(cols)
}
pub fn format_names() -> Vec<String> {
let mut names: Vec<String> = vec![
"long".into(), "long2".into(), "long3".into(),
];
if let Some(ref cfg) = *crate::config::CONFIG {
for name in cfg.format.keys() {
if !names.iter().any(|n| n == name) {
names.push(name.clone());
}
}
}
names
}
fn deduce_columns(matches: &MatchedFlags, long_count: u8) -> Vec<Column> {
if let Some(cols_str) = matches.get(flags::COLUMNS) {
let mut columns = Vec::new();
for name in cols_str.split(',') {
let name = name.trim();
if let Some(col) = Column::from_name(name)
&& !columns.contains(&col) {
columns.push(col);
}
}
apply_individual_adds(matches, &mut columns);
apply_suppressions(matches, &mut columns);
return columns;
}
if let Some(fmt_name) = matches.get(flags::FORMAT)
&& let Some(cols) = format_columns(fmt_name) {
let mut columns = cols;
apply_individual_adds(matches, &mut columns);
apply_suppressions(matches, &mut columns);
return columns;
}
let tier_name = match long_count {
0 | 1 => "long",
2 => "long2",
_ => "long3",
};
let mut columns = format_columns(tier_name)
.expect("compiled-in format always exists");
apply_individual_adds(matches, &mut columns);
apply_timestamp_overrides(matches, &mut columns);
apply_suppressions(matches, &mut columns);
columns
}
const CANONICAL_ORDER: &[Column] = &[
Column::Inode,
Column::Octal,
Column::Permissions,
Column::HardLinks,
Column::FileSize,
Column::Blocks,
Column::User,
Column::Group,
Column::Timestamp(TimeType::Modified),
Column::Timestamp(TimeType::Changed),
Column::Timestamp(TimeType::Created),
Column::Timestamp(TimeType::Accessed),
Column::VcsStatus,
Column::VcsRepos,
];
fn canonical_insert_pos(columns: &[Column], col: Column) -> usize {
let canon_idx = CANONICAL_ORDER.iter()
.position(|c| *c == col)
.unwrap_or(CANONICAL_ORDER.len());
let mut best_pos = 0;
for (i, existing) in columns.iter().enumerate() {
let existing_idx = CANONICAL_ORDER.iter()
.position(|c| c == existing)
.unwrap_or(CANONICAL_ORDER.len());
if existing_idx < canon_idx {
best_pos = i + 1;
}
}
best_pos
}
fn apply_individual_adds(matches: &MatchedFlags, columns: &mut Vec<Column>) {
let adds: &[(bool, Column)] = &[
(matches.has(flags::INODE), Column::Inode),
(matches.has(flags::LINKS), Column::HardLinks),
(matches.has(flags::BLOCKS), Column::Blocks),
(matches.has(flags::GROUP), Column::Group),
(matches.has(flags::OCTAL), Column::Octal),
(matches.has(flags::VCS_STATUS), Column::VcsStatus),
(matches.has(flags::VCS_REPOS), Column::VcsRepos),
];
for &(enabled, col) in adds {
if enabled && !columns.contains(&col) {
let pos = canonical_insert_pos(columns, col);
columns.insert(pos, col);
}
}
}
fn apply_timestamp_overrides(matches: &MatchedFlags, columns: &mut Vec<Column>) {
let has_explicit_time = matches.has(flags::MODIFIED) || matches.has(flags::CHANGED)
|| matches.has(flags::ACCESSED) || matches.has(flags::CREATED)
|| matches.get(flags::TIME).is_some();
if !has_explicit_time {
return;
}
columns.retain(|c| !matches!(c, Column::Timestamp(_)));
if matches.has(flags::MODIFIED) || matches.get(flags::TIME).is_some_and(|v| v == "modified" || v == "mod") {
columns.insert(timestamp_insert_pos(columns), Column::Timestamp(TimeType::Modified));
}
if matches.has(flags::CHANGED) || matches.get(flags::TIME).is_some_and(|v| v == "changed" || v == "ch") {
columns.insert(timestamp_insert_pos(columns), Column::Timestamp(TimeType::Changed));
}
if matches.has(flags::ACCESSED) || matches.get(flags::TIME).is_some_and(|v| v == "accessed" || v == "acc") {
columns.insert(timestamp_insert_pos(columns), Column::Timestamp(TimeType::Accessed));
}
if matches.has(flags::CREATED) || matches.get(flags::TIME).is_some_and(|v| v == "created" || v == "cr") {
columns.insert(timestamp_insert_pos(columns), Column::Timestamp(TimeType::Created));
}
}
fn apply_suppressions(matches: &MatchedFlags, columns: &mut Vec<Column>) {
if matches.has(flags::NO_PERMISSIONS) && !matches.has(flags::SHOW_PERMISSIONS) {
columns.retain(|c| *c != Column::Permissions);
}
if matches.has(flags::NO_FILESIZE) && !matches.has(flags::SHOW_FILESIZE) {
columns.retain(|c| *c != Column::FileSize);
}
#[cfg(unix)]
if matches.has(flags::NO_USER) && !matches.has(flags::SHOW_USER) {
columns.retain(|c| *c != Column::User);
}
if matches.has(flags::NO_TIME) {
columns.retain(|c| !matches!(c, Column::Timestamp(_)));
}
#[cfg(unix)]
if matches.has(flags::NO_INODE) { columns.retain(|c| *c != Column::Inode); }
#[cfg(unix)]
if matches.has(flags::NO_GROUP) { columns.retain(|c| *c != Column::Group); }
#[cfg(unix)]
if matches.has(flags::NO_LINKS) { columns.retain(|c| *c != Column::HardLinks); }
#[cfg(unix)]
if matches.has(flags::NO_BLOCKS) { columns.retain(|c| *c != Column::Blocks); }
if matches.has(flags::SHOW_PERMISSIONS) && !columns.contains(&Column::Permissions) {
columns.insert(0, Column::Permissions);
}
if matches.has(flags::SHOW_FILESIZE) && !columns.contains(&Column::FileSize) {
let pos = columns.iter()
.position(|c| matches!(c, Column::User | Column::Group | Column::Timestamp(_) | Column::VcsStatus))
.unwrap_or(columns.len());
columns.insert(pos, Column::FileSize);
}
#[cfg(unix)]
if matches.has(flags::SHOW_USER) && !columns.contains(&Column::User) {
let pos = columns.iter()
.position(|c| matches!(c, Column::Group | Column::Timestamp(_) | Column::VcsStatus))
.unwrap_or(columns.len());
columns.insert(pos, Column::User);
}
}
fn timestamp_insert_pos(columns: &[Column]) -> usize {
let last_ts = columns.iter().rposition(|c| matches!(c, Column::Timestamp(_)));
if let Some(pos) = last_ts {
return pos + 1;
}
columns.iter()
.position(|c| *c == Column::VcsStatus)
.unwrap_or(columns.len())
}
impl SizeFormat {
fn deduce(matches: &MatchedFlags) -> Self {
if matches.has(flags::BINARY) {
Self::BinaryBytes
}
else if matches.has(flags::BYTES) {
Self::JustBytes
}
else {
Self::DecimalBytes
}
}
}
impl TimeFormat {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
if let Some(w) = matches.get(flags::TIME_STYLE) {
return Ok(Self::from_str(w));
}
use crate::options::vars;
match vars.get(vars::TIME_STYLE) {
Some(ref t) if ! t.is_empty() => {
Ok(Self::from_str(&t.to_string_lossy()))
}
_ => Ok(Self::DefaultFormat),
}
}
fn from_str(word: &str) -> Self {
match word {
"default" => Self::DefaultFormat,
"iso" => Self::ISOFormat,
"long-iso" => Self::LongISO,
"full-iso" => Self::FullISO,
"relative" => Self::Relative,
s if s.starts_with('+') => Self::Custom(s[1..].to_string()),
_ => Self::DefaultFormat,
}
}
}
impl UserFormat {
fn deduce(matches: &MatchedFlags) -> Self {
if matches.has(flags::NUMERIC) { Self::Numeric } else { Self::Name }
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::options::test::parse_for_test;
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr; $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), |mf| $type::deduce(mf)) {
assert_eq!(result, $result);
}
}
};
($name:ident: $type:ident <- $inputs:expr, $vars:expr; like $pat:pat) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), |mf| $type::deduce(mf, &$vars)) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
_ => assert!(false),
}
}
}
};
}
mod size_formats {
use super::*;
test!(empty: SizeFormat <- []; SizeFormat::DecimalBytes);
test!(binary: SizeFormat <- ["--binary"]; SizeFormat::BinaryBytes);
test!(bytes: SizeFormat <- ["--bytes"]; SizeFormat::JustBytes);
test!(both_1: SizeFormat <- ["--binary", "--binary"]; SizeFormat::BinaryBytes);
test!(both_2: SizeFormat <- ["--bytes", "--binary"]; SizeFormat::BinaryBytes);
test!(both_3: SizeFormat <- ["--binary", "--bytes"]; SizeFormat::JustBytes);
test!(both_4: SizeFormat <- ["--bytes", "--bytes"]; SizeFormat::JustBytes);
}
mod time_formats {
use super::*;
test!(empty: TimeFormat <- [], None; like Ok(TimeFormat::DefaultFormat));
test!(default: TimeFormat <- ["--time-style=default"], None; like Ok(TimeFormat::DefaultFormat));
test!(iso: TimeFormat <- ["--time-style", "iso"], None; like Ok(TimeFormat::ISOFormat));
test!(long_iso: TimeFormat <- ["--time-style=long-iso"], None; like Ok(TimeFormat::LongISO));
test!(full_iso: TimeFormat <- ["--time-style", "full-iso"], None; like Ok(TimeFormat::FullISO));
test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; like Ok(TimeFormat::ISOFormat));
test!(nevermind: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; like Ok(TimeFormat::FullISO));
test!(relative: TimeFormat <- ["--time-style=relative"], None; like Ok(TimeFormat::Relative));
test!(custom: TimeFormat <- ["--time-style=+%Y-%m-%d"], None; like Ok(TimeFormat::Custom(_)));
test!(unknown: TimeFormat <- ["--time-style=24-hour"], None; like Ok(TimeFormat::DefaultFormat));
test!(use_env: TimeFormat <- [], Some("long-iso".into()); like Ok(TimeFormat::LongISO));
test!(override_env: TimeFormat <- ["--time-style=full-iso"], Some("long-iso".into()); like Ok(TimeFormat::FullISO));
}
mod columns {
use crate::options::test::parse_for_test;
use crate::output::table::{Column, TimeType};
use super::deduce_columns;
fn timestamps(inputs: &[&str], long_count: u8) -> Vec<Column> {
parse_for_test(inputs, |mf| deduce_columns(mf, long_count))
.into_iter().next().unwrap()
.into_iter()
.filter(|c| matches!(c, Column::Timestamp(_)))
.collect()
}
#[test]
fn default_has_modified() {
let ts = timestamps(&[], 1);
assert_eq!(ts, vec![Column::Timestamp(TimeType::Modified)]);
}
#[test]
fn explicit_modified() {
let ts = timestamps(&["--modified"], 1);
assert_eq!(ts, vec![Column::Timestamp(TimeType::Modified)]);
}
#[test]
fn explicit_accessed() {
let ts = timestamps(&["-u"], 1);
assert_eq!(ts, vec![Column::Timestamp(TimeType::Accessed)]);
}
#[test]
fn explicit_created() {
let ts = timestamps(&["-U"], 1);
assert_eq!(ts, vec![Column::Timestamp(TimeType::Created)]);
}
#[test]
fn time_param_modified() {
let ts = timestamps(&["--time=modified"], 1);
assert_eq!(ts, vec![Column::Timestamp(TimeType::Modified)]);
}
#[test]
fn time_param_accessed() {
let ts = timestamps(&["-t", "acc"], 1);
assert_eq!(ts, vec![Column::Timestamp(TimeType::Accessed)]);
}
#[test]
fn multiple_timestamps() {
let ts = timestamps(&["-u", "--modified"], 1);
assert!(ts.contains(&Column::Timestamp(TimeType::Modified)));
assert!(ts.contains(&Column::Timestamp(TimeType::Accessed)));
}
#[test]
fn tier3_all_timestamps() {
let ts = timestamps(&[], 3);
assert_eq!(ts.len(), 4);
assert!(ts.contains(&Column::Timestamp(TimeType::Modified)));
assert!(ts.contains(&Column::Timestamp(TimeType::Changed)));
assert!(ts.contains(&Column::Timestamp(TimeType::Accessed)));
assert!(ts.contains(&Column::Timestamp(TimeType::Created)));
}
#[test]
fn no_time_suppresses_all() {
let ts = timestamps(&["--no-time"], 3);
assert!(ts.is_empty());
}
#[test]
fn tier2_has_vcs_and_group() {
let cols: Vec<Column> = parse_for_test(&[], |mf| deduce_columns(mf, 2))
.into_iter().next().unwrap();
assert!(cols.contains(&Column::VcsStatus));
assert!(cols.contains(&Column::Group));
}
#[test]
fn tier3_has_links_and_blocks() {
let cols: Vec<Column> = parse_for_test(&[], |mf| deduce_columns(mf, 3))
.into_iter().next().unwrap();
assert!(cols.contains(&Column::HardLinks));
assert!(cols.contains(&Column::Blocks));
}
#[test]
fn no_group_suppresses() {
let cols: Vec<Column> = parse_for_test(&["--no-group"], |mf| deduce_columns(mf, 2))
.into_iter().next().unwrap();
assert!(!cols.contains(&Column::Group));
}
#[test]
fn time_tea() {
let cmd = crate::options::parser::build_command();
assert!(cmd.try_get_matches_from(["lx", "--time=tea"]).is_err());
}
#[test]
fn t_ea() {
let cmd = crate::options::parser::build_command();
assert!(cmd.try_get_matches_from(["lx", "-tea"]).is_err());
}
}
mod views {
use super::*;
use crate::output::grid::Options as GridOptions;
test!(empty: Mode <- [], None; like Ok(Mode::Grid(_)));
test!(original_g: Mode <- ["-G"], None; like Ok(Mode::Grid(GridOptions { across: false, .. })));
test!(grid: Mode <- ["--grid"], None; like Ok(Mode::Grid(GridOptions { across: false, .. })));
test!(across: Mode <- ["--across"], None; like Ok(Mode::Grid(GridOptions { across: true, .. })));
test!(gracross: Mode <- ["-xG"], None; like Ok(Mode::Grid(GridOptions { across: true, .. })));
test!(lines: Mode <- ["--oneline"], None; like Ok(Mode::Lines));
test!(prima: Mode <- ["-1"], None; like Ok(Mode::Lines));
test!(long: Mode <- ["--long"], None; like Ok(Mode::Details(_)));
test!(ell: Mode <- ["-l"], None; like Ok(Mode::Details(_)));
test!(lid: Mode <- ["--long", "--grid"], None; like Ok(Mode::GridDetails(_)));
test!(leg: Mode <- ["-lG"], None; like Ok(Mode::GridDetails(_)));
test!(long_across: Mode <- ["--long", "--across"], None; like Ok(Mode::Details(_)));
test!(just_header: Mode <- ["--header"], None; like Ok(Mode::Grid(_)));
test!(just_group: Mode <- ["--group"], None; like Ok(Mode::Grid(_)));
test!(just_inode: Mode <- ["--inode"], None; like Ok(Mode::Grid(_)));
test!(just_links: Mode <- ["--links"], None; like Ok(Mode::Grid(_)));
test!(just_blocks: Mode <- ["--blocks"], None; like Ok(Mode::Grid(_)));
test!(just_binary: Mode <- ["--binary"], None; like Ok(Mode::Grid(_)));
test!(just_bytes: Mode <- ["--bytes"], None; like Ok(Mode::Grid(_)));
test!(just_numeric: Mode <- ["--numeric"], None; like Ok(Mode::Grid(_)));
#[cfg(feature = "git")]
test!(just_vcs_status: Mode <- ["--vcs-status"], None; like Ok(Mode::Grid(_)));
test!(lgo: Mode <- ["--long", "--grid", "--oneline"], None; like Ok(Mode::Lines));
test!(lgt: Mode <- ["--long", "--grid", "--tree"], None; like Ok(Mode::Details(_)));
test!(tgl: Mode <- ["--tree", "--grid", "--long"], None; like Ok(Mode::Details(_)));
test!(tlg: Mode <- ["--tree", "--long", "--grid"], None; like Ok(Mode::Details(_)));
test!(ot: Mode <- ["--oneline", "--tree"], None; like Ok(Mode::Details(_)));
test!(og: Mode <- ["--oneline", "--grid"], None; like Ok(Mode::Grid(_)));
test!(tg: Mode <- ["--tree", "--grid"], None; like Ok(Mode::Details(_)));
}
}