use std::{
borrow::Cow,
ffi::{OsStr, OsString},
io::{IsTerminal, stdout},
num::IntErrorKind,
};
use glob::Pattern;
use lscolors::LsColors;
use term_grid::SPACES_IN_TAB;
use uucore::{
display::Quotable, error::UResult, format::human::SizeFormat, fsext::MetadataTimeField,
line_ending::LineEnding, parser::parse_block_size, parser::parse_glob,
parser::parse_size::parse_size_non_zero_u64, quoting_style::QuotingStyle, show_error,
show_warning, time::format, translate,
};
use crate::{
LsError,
colors::{LsColorsParseError, validate_ls_colors_env},
dired::is_dired_arg_present,
display::{Format, IndicatorStyle, LocaleQuoting, LongFormat},
options::QUOTING_STYLE,
};
pub mod options {
pub mod format {
pub static ONE_LINE: &str = "1";
pub static LONG: &str = "long";
pub static COLUMNS: &str = "C";
pub static ACROSS: &str = "x";
pub static TAB_SIZE: &str = "tabsize";
pub static COMMAS: &str = "m";
pub static LONG_NO_OWNER: &str = "g";
pub static LONG_NO_GROUP: &str = "o";
pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid";
}
pub mod files {
pub static ALL: &str = "all";
pub static ALMOST_ALL: &str = "almost-all";
pub static UNSORTED_ALL: &str = "f";
}
pub mod sort {
pub static SIZE: &str = "S";
pub static TIME: &str = "t";
pub static NONE: &str = "U";
pub static VERSION: &str = "v";
pub static EXTENSION: &str = "X";
}
pub mod time {
pub static ACCESS: &str = "u";
pub static CHANGE: &str = "c";
}
pub mod size {
pub static ALLOCATION_SIZE: &str = "size";
pub static BLOCK_SIZE: &str = "block-size";
pub static HUMAN_READABLE: &str = "human-readable";
pub static SI: &str = "si";
pub static KIBIBYTES: &str = "kibibytes";
}
pub mod quoting {
pub static ESCAPE: &str = "escape";
pub static LITERAL: &str = "literal";
pub static C: &str = "quote-name";
}
pub mod indicator_style {
pub static SLASH: &str = "p";
pub static FILE_TYPE: &str = "file-type";
pub static CLASSIFY: &str = "classify";
}
pub mod dereference {
pub static ALL: &str = "dereference";
pub static ARGS: &str = "dereference-command-line";
pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir";
}
pub static HELP: &str = "help";
pub static QUOTING_STYLE: &str = "quoting-style";
pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars";
pub static SHOW_CONTROL_CHARS: &str = "show-control-chars";
pub static WIDTH: &str = "width";
pub static AUTHOR: &str = "author";
pub static NO_GROUP: &str = "no-group";
pub static FORMAT: &str = "format";
pub static SORT: &str = "sort";
pub static TIME: &str = "time";
pub static IGNORE_BACKUPS: &str = "ignore-backups";
pub static DIRECTORY: &str = "directory";
pub static INODE: &str = "inode";
pub static REVERSE: &str = "reverse";
pub static RECURSIVE: &str = "recursive";
pub static COLOR: &str = "color";
pub static PATHS: &str = "paths";
pub static INDICATOR_STYLE: &str = "indicator-style";
pub static TIME_STYLE: &str = "time-style";
pub static FULL_TIME: &str = "full-time";
pub static HIDE: &str = "hide";
pub static IGNORE: &str = "ignore";
pub static CONTEXT: &str = "context";
pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first";
pub static ZERO: &str = "zero";
pub static DIRED: &str = "dired";
pub static HYPERLINK: &str = "hyperlink";
}
const DEFAULT_TERM_WIDTH: u16 = 80;
const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512;
const DEFAULT_BLOCK_SIZE: u64 = 1024;
const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1;
fn resolve_block_sizes_from_env(opt_kb: bool) -> (u64, u64) {
match parse_block_size::block_size_from_env(&["LS_BLOCK_SIZE", "BLOCK_SIZE"]) {
parse_block_size::BlockSizeEnv::Found(size) => {
if opt_kb {
(size, DEFAULT_BLOCK_SIZE)
} else {
(size, size)
}
}
parse_block_size::BlockSizeEnv::SetButInvalid => (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE),
parse_block_size::BlockSizeEnv::NotSet => {
match parse_block_size::block_size_from_env(&["BLOCKSIZE"]) {
parse_block_size::BlockSizeEnv::Found(size) => {
if opt_kb {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE)
} else {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, size)
}
}
parse_block_size::BlockSizeEnv::SetButInvalid => {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE)
}
parse_block_size::BlockSizeEnv::NotSet => {
if std::env::var_os("POSIXLY_CORRECT").is_some() && !opt_kb {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE)
} else {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE)
}
}
}
}
}
}
pub(crate) enum Dereference {
None,
DirArgs,
Args,
All,
}
#[derive(PartialEq, Eq)]
pub(crate) enum Sort {
None,
Name,
Size,
Time,
Version,
Extension,
Width,
}
#[derive(PartialEq, Eq)]
pub(crate) enum Files {
All,
AlmostAll,
Normal,
}
pub struct Config {
pub format: Format,
pub(crate) files: Files,
pub(crate) sort: Sort,
pub(crate) recursive: bool,
pub(crate) reverse: bool,
pub(crate) dereference: Dereference,
pub(crate) ignore_patterns: Vec<Pattern>,
pub(crate) size_format: SizeFormat,
pub(crate) directory: bool,
pub(crate) time: MetadataTimeField,
#[cfg(unix)]
pub(crate) inode: bool,
pub(crate) color: Option<LsColors>,
pub(crate) long: LongFormat,
pub(crate) alloc_size: bool,
pub(crate) file_size_block_size: u64,
#[allow(dead_code)]
pub(crate) block_size: u64, pub(crate) width: u16,
pub quoting_style: QuotingStyle,
pub(crate) locale_quoting: Option<LocaleQuoting>,
pub(crate) indicator_style: Option<IndicatorStyle>,
pub(crate) time_format_recent: String, pub(crate) time_format_older: Option<String>, pub(crate) context: bool,
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
pub(crate) selinux_supported: bool,
#[cfg(all(feature = "smack", target_os = "linux"))]
pub(crate) smack_supported: bool,
pub(crate) group_directories_first: bool,
pub(crate) line_ending: LineEnding,
pub(crate) dired: bool,
pub(crate) hyperlink: bool,
pub(crate) tab_size: usize,
}
fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) {
if let Some(format_) = options.get_one::<String>(options::FORMAT) {
(
match format_.as_str() {
"long" | "verbose" => Format::Long,
"single-column" => Format::OneLine,
"columns" | "vertical" => Format::Columns,
"across" | "horizontal" => Format::Across,
"commas" => Format::Commas,
_ => unreachable!("Invalid field for --format"),
},
Some(options::FORMAT),
)
} else if options.get_flag(options::format::LONG) {
(Format::Long, Some(options::format::LONG))
} else if options.get_flag(options::format::ACROSS) {
(Format::Across, Some(options::format::ACROSS))
} else if options.get_flag(options::format::COMMAS) {
(Format::Commas, Some(options::format::COMMAS))
} else if options.get_flag(options::format::COLUMNS) {
(Format::Columns, Some(options::format::COLUMNS))
} else if stdout().is_terminal() {
(Format::Columns, None)
} else {
(Format::OneLine, None)
}
}
fn extract_files(options: &clap::ArgMatches) -> Files {
let get_last_index = |flag: &str| -> usize {
if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) {
options.index_of(flag).unwrap_or(0)
} else {
0
}
};
let all_index = get_last_index(options::files::ALL);
let almost_all_index = get_last_index(options::files::ALMOST_ALL);
let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL);
let max_index = all_index.max(almost_all_index).max(unsorted_all_index);
if max_index == 0 {
Files::Normal
} else if max_index == almost_all_index {
Files::AlmostAll
} else {
Files::All
}
}
fn extract_sort(options: &clap::ArgMatches) -> Sort {
let get_last_index = |flag: &str| -> usize {
if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) {
options.index_of(flag).unwrap_or(0)
} else {
0
}
};
let sort_index = options
.indices_of(options::SORT)
.and_then(|mut it| it.next_back())
.unwrap_or(0);
let time_index = get_last_index(options::sort::TIME);
let size_index = get_last_index(options::sort::SIZE);
let none_index = get_last_index(options::sort::NONE);
let version_index = get_last_index(options::sort::VERSION);
let extension_index = get_last_index(options::sort::EXTENSION);
let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL);
let max_sort_index = sort_index
.max(time_index)
.max(size_index)
.max(none_index)
.max(version_index)
.max(extension_index)
.max(unsorted_all_index);
match max_sort_index {
0 => {
if !options.get_flag(options::format::LONG)
&& (options.get_flag(options::time::ACCESS)
|| options.get_flag(options::time::CHANGE)
|| options.get_one::<String>(options::TIME).is_some())
{
Sort::Time
} else {
Sort::Name
}
}
idx if idx == unsorted_all_index || idx == none_index => Sort::None,
idx if idx == sort_index => {
if let Some(field) = options.get_one::<String>(options::SORT) {
match field.as_str() {
"none" => Sort::None,
"name" => Sort::Name,
"time" => Sort::Time,
"size" => Sort::Size,
"version" => Sort::Version,
"extension" => Sort::Extension,
"width" => Sort::Width,
_ => unreachable!("Invalid field for --sort"),
}
} else {
Sort::Name
}
}
idx if idx == time_index => Sort::Time,
idx if idx == size_index => Sort::Size,
idx if idx == version_index => Sort::Version,
idx if idx == extension_index => Sort::Extension,
_ => Sort::Name,
}
}
fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField {
if let Some(field) = options.get_one::<String>(options::TIME) {
field.as_str().into()
} else if options.get_flag(options::time::ACCESS) {
MetadataTimeField::Access
} else if options.get_flag(options::time::CHANGE) {
MetadataTimeField::Change
} else {
MetadataTimeField::Modification
}
}
fn is_color_compatible_term() -> bool {
let term = std::env::var_os("TERM");
let colorterm = std::env::var_os("COLORTERM");
let term_matches = |term: &OsStr| -> bool {
uucore::colors::TERMS.iter().any(|&pattern| {
term == pattern
|| (pattern.ends_with('*')
&& term
.as_encoded_bytes()
.starts_with(&pattern.as_bytes()[..pattern.len() - 1]))
})
};
match (term, colorterm) {
(Some(t), Some(c)) if t.is_empty() && c.is_empty() => false,
(Some(t), _) if !t.is_empty() => term_matches(&t),
_ => true,
}
}
fn extract_color(options: &clap::ArgMatches) -> bool {
if !is_color_compatible_term() {
return false;
}
let get_last_index = |flag: &str| -> usize {
if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) {
options.index_of(flag).unwrap_or(0)
} else {
0
}
};
let color_index = options
.indices_of(options::COLOR)
.and_then(|mut it| it.next_back())
.unwrap_or(0);
let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL);
let color_enabled = match options.get_one::<String>(options::COLOR) {
None => options.contains_id(options::COLOR),
Some(val) => match val.as_str() {
"" | "always" | "yes" | "force" => true,
"auto" | "tty" | "if-tty" => stdout().is_terminal(),
_ => false,
},
};
if color_index > 0 {
color_enabled
} else if unsorted_all_index > 0 {
false
} else {
color_enabled
}
}
fn extract_hyperlink(options: &clap::ArgMatches) -> bool {
let hyperlink = options
.get_one::<String>(options::HYPERLINK)
.unwrap()
.as_str();
match hyperlink {
"always" | "yes" | "force" => true,
"auto" | "tty" | "if-tty" => stdout().is_terminal(),
"never" | "no" | "none" => false,
_ => unreachable!("should be handled by clap"),
}
}
struct QuotingStyleSpec {
style: QuotingStyle,
fixed_control: bool,
locale: Option<LocaleQuoting>,
}
impl QuotingStyleSpec {
fn new(style: QuotingStyle) -> Self {
Self {
style,
fixed_control: false,
locale: None,
}
}
fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self {
Self {
style,
fixed_control: true,
locale: Some(locale),
}
}
}
fn match_quoting_style_name(
style: &str,
show_control: bool,
) -> Option<(QuotingStyle, Option<LocaleQuoting>)> {
let spec = match style {
"literal" => QuotingStyleSpec::new(QuotingStyle::Literal {
show_control: false,
}),
"shell" => QuotingStyleSpec::new(QuotingStyle::SHELL),
"shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE),
"shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE),
"shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE),
"c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE),
"escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES),
"locale" => QuotingStyleSpec {
style: QuotingStyle::Literal {
show_control: false,
},
fixed_control: true,
locale: Some(LocaleQuoting::Single),
},
"clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double),
_ => return None,
};
let style = if spec.fixed_control {
spec.style
} else {
spec.style.show_control(show_control)
};
Some((style, spec.locale))
}
fn extract_quoting_style(
options: &clap::ArgMatches,
show_control: bool,
) -> (QuotingStyle, Option<LocaleQuoting>) {
let opt_quoting_style = options.get_one::<String>(QUOTING_STYLE);
if let Some(style) = opt_quoting_style {
match match_quoting_style_name(style, show_control) {
Some(pair) => pair,
None => unreachable!("Should have been caught by Clap"),
}
} else if options.get_flag(options::quoting::LITERAL) {
(QuotingStyle::Literal { show_control }, None)
} else if options.get_flag(options::quoting::ESCAPE) {
(QuotingStyle::C_NO_QUOTES, None)
} else if options.get_flag(options::quoting::C) {
(QuotingStyle::C_DOUBLE, None)
} else if options.get_flag(options::DIRED) {
(QuotingStyle::Literal { show_control }, None)
} else {
if let Ok(style) = std::env::var("QUOTING_STYLE") {
match match_quoting_style_name(style.as_str(), show_control) {
Some(pair) => return pair,
None => eprintln!(
"{}",
translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone())
),
}
}
if stdout().is_terminal() {
(QuotingStyle::SHELL_ESCAPE.show_control(show_control), None)
} else {
(QuotingStyle::Literal { show_control }, None)
}
}
}
fn extract_indicator_style(options: &clap::ArgMatches) -> Option<IndicatorStyle> {
if let Some(field) = options.get_one::<String>(options::INDICATOR_STYLE) {
match field.as_str() {
"file-type" => Some(IndicatorStyle::FileType),
"classify" => Some(IndicatorStyle::Classify),
"slash" => Some(IndicatorStyle::Slash),
"none" | &_ => None,
}
} else if let Some(field) = options.get_one::<String>(options::indicator_style::CLASSIFY) {
match field.as_str() {
"always" | "yes" | "force" => Some(IndicatorStyle::Classify),
"auto" | "tty" | "if-tty" => stdout().is_terminal().then_some(IndicatorStyle::Classify),
"never" | "no" | "none" | &_ => None,
}
} else if options.get_flag(options::indicator_style::SLASH) {
Some(IndicatorStyle::Slash)
} else if options.get_flag(options::indicator_style::FILE_TYPE) {
Some(IndicatorStyle::FileType)
} else {
None
}
}
fn parse_width(width_match: Option<&String>) -> Result<u16, LsError> {
let parse_width_from_args = |s: &str| -> Result<u16, LsError> {
let radix = if s.starts_with('0') && s.len() > 1 {
8
} else {
10
};
match u16::from_str_radix(s, radix) {
Ok(x) => Ok(x),
Err(e) => match e.kind() {
IntErrorKind::PosOverflow => Ok(u16::MAX),
_ => Err(LsError::InvalidLineWidth(s.into())),
},
}
};
let parse_width_from_env = |columns: OsString| {
if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) {
columns
} else {
show_error!(
"{}",
translate!("ls-invalid-columns-width", "width" => columns.quote())
);
DEFAULT_TERM_WIDTH
}
};
let calculate_term_size = || match terminal_size::terminal_size() {
Some((width, _)) => width.0,
None => DEFAULT_TERM_WIDTH,
};
let ret = match width_match {
Some(x) => parse_width_from_args(x)?,
None => match std::env::var_os("COLUMNS") {
Some(columns) => parse_width_from_env(columns),
None => calculate_term_size(),
},
};
Ok(ret)
}
fn parse_tab_size(size_str: &str) -> Result<usize, LsError> {
size_str
.parse::<usize>()
.ok()
.or_else(|| {
size_str
.strip_prefix("0x")
.or_else(|| size_str.strip_prefix("0X"))
.and_then(|hex| usize::from_str_radix(hex, 16).ok())
})
.ok_or_else(|| LsError::InvalidTabSize(size_str.to_string()))
}
impl Config {
#[allow(clippy::cognitive_complexity)]
pub fn from(options: &clap::ArgMatches) -> UResult<Self> {
let context = options.get_flag(options::CONTEXT);
let (mut format, opt) = extract_format(options);
let files = extract_files(options);
if format != Format::Long {
let idx = opt
.and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap()))
.unwrap_or(0);
if [
options::format::LONG_NO_OWNER,
options::format::LONG_NO_GROUP,
options::format::LONG_NUMERIC_UID_GID,
options::FULL_TIME,
]
.iter()
.filter_map(|opt| {
if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) {
options.indices_of(opt)
} else {
None
}
})
.flatten()
.any(|i| i >= idx)
{
format = Format::Long;
} else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE)
&& options.value_source(options::format::ONE_LINE)
== Some(clap::parser::ValueSource::CommandLine)
&& indices.any(|i| i > idx)
{
format = Format::OneLine;
}
}
let sort = extract_sort(options);
let time = extract_time(options);
let mut needs_color = extract_color(options);
let hyperlink = extract_hyperlink(options);
let opt_block_size = options.get_one::<String>(options::size::BLOCK_SIZE);
let opt_si = opt_block_size.is_some_and(|x| x == options::size::SI)
|| options.get_flag(options::size::SI);
let opt_hr = opt_block_size.is_some_and(|x| x == options::size::HUMAN_READABLE)
|| options.get_flag(options::size::HUMAN_READABLE);
let opt_kb = options.get_flag(options::size::KIBIBYTES);
let size_format = if opt_si {
SizeFormat::Decimal
} else if opt_hr {
SizeFormat::Binary
} else {
SizeFormat::Bytes
};
let (file_size_block_size, block_size) = if let Some(opt_block_size) = opt_block_size {
if opt_si {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000)
} else if opt_hr {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE)
} else if let Ok(size) = parse_size_non_zero_u64(opt_block_size) {
(size, size)
} else {
return Err(Box::new(LsError::BlockSizeParseError(
opt_block_size.clone(),
)));
}
} else if !opt_si && !opt_hr {
resolve_block_sizes_from_env(opt_kb)
} else if opt_si {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000)
} else {
(DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE)
};
let long = {
let author = options.get_flag(options::AUTHOR);
let group = !options.get_flag(options::NO_GROUP)
&& !options.get_flag(options::format::LONG_NO_GROUP);
let owner = !options.get_flag(options::format::LONG_NO_OWNER);
let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID);
LongFormat {
author,
group,
owner,
numeric_uid_gid,
}
};
let width = parse_width(options.get_one::<String>(options::WIDTH))?;
let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) {
false
} else {
options.get_flag(options::SHOW_CONTROL_CHARS) || !stdout().is_terminal()
};
let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control);
let indicator_style = extract_indicator_style(options);
let dired = options.get_flag(options::DIRED);
let (time_format_recent, time_format_older) = if format == Format::Long || dired {
parse_time_style(options)?
} else {
Default::default()
};
let mut ignore_patterns: Vec<Pattern> = Vec::new();
if options.get_flag(options::IGNORE_BACKUPS) {
ignore_patterns.push(Pattern::new("*~").unwrap());
ignore_patterns.push(Pattern::new(".*~").unwrap());
}
for pattern in options
.get_many::<String>(options::IGNORE)
.into_iter()
.flatten()
{
if let Ok(p) = parse_glob::from_str(pattern) {
ignore_patterns.push(p);
} else {
show_warning!(
"{}",
translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote())
);
}
}
if files == Files::Normal {
for pattern in options
.get_many::<String>(options::HIDE)
.into_iter()
.flatten()
{
if let Ok(p) = parse_glob::from_str(pattern) {
ignore_patterns.push(p);
} else {
show_warning!(
"{}",
translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote())
);
}
}
}
let zero_formats_opts = [
options::format::ACROSS,
options::format::COLUMNS,
options::format::COMMAS,
options::format::LONG,
options::format::LONG_NO_GROUP,
options::format::LONG_NO_OWNER,
options::format::LONG_NUMERIC_UID_GID,
options::format::ONE_LINE,
options::FORMAT,
];
let zero_colors_opts = [options::COLOR];
let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS];
let zero_quoting_style_opts = [
QUOTING_STYLE,
options::quoting::C,
options::quoting::ESCAPE,
options::quoting::LITERAL,
];
let get_last = |flag: &str| -> usize {
if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) {
options.index_of(flag).unwrap_or(0)
} else {
0
}
};
if get_last(options::ZERO)
> zero_formats_opts
.into_iter()
.map(get_last)
.max()
.unwrap_or(0)
{
format = if format == Format::Long {
format
} else {
Format::OneLine
};
}
if get_last(options::ZERO)
> zero_colors_opts
.into_iter()
.map(get_last)
.max()
.unwrap_or(0)
{
needs_color = false;
}
if get_last(options::ZERO)
> zero_show_control_opts
.into_iter()
.map(get_last)
.max()
.unwrap_or(0)
{
show_control = true;
}
if get_last(options::ZERO)
> zero_quoting_style_opts
.into_iter()
.map(get_last)
.max()
.unwrap_or(0)
{
quoting_style = QuotingStyle::Literal { show_control };
locale_quoting = None;
}
if needs_color && let Err(err) = validate_ls_colors_env() {
if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err {
show_warning!(
"{}",
translate!(
"ls-warning-unrecognized-ls-colors-prefix",
"prefix" => prefix.quote()
)
);
}
show_warning!("{}", translate!("ls-warning-unparsable-ls-colors"));
needs_color = false;
}
let color = if needs_color {
Some(LsColors::from_env().unwrap_or_default())
} else {
None
};
if dired || is_dired_arg_present() {
format = Format::Long;
}
if dired && options.get_flag(options::ZERO) {
return Err(Box::new(LsError::DiredAndZeroAreIncompatible));
}
let dereference = if options.get_flag(options::dereference::ALL) {
Dereference::All
} else if options.get_flag(options::dereference::ARGS) {
Dereference::Args
} else if options.get_flag(options::dereference::DIR_ARGS) {
Dereference::DirArgs
} else if options.get_flag(options::DIRECTORY)
|| indicator_style == Some(IndicatorStyle::Classify)
|| format == Format::Long
{
Dereference::None
} else {
Dereference::DirArgs
};
let tab_size = if needs_color {
Some(0)
} else if let Some(size_str) = options.get_one::<String>(options::format::TAB_SIZE) {
Some(parse_tab_size(size_str)?)
} else {
None
};
Ok(Self {
format,
files,
sort,
recursive: options.get_flag(options::RECURSIVE),
reverse: options.get_flag(options::REVERSE),
dereference,
ignore_patterns,
size_format,
directory: options.get_flag(options::DIRECTORY),
time,
color,
#[cfg(unix)]
inode: options.get_flag(options::INODE),
long,
alloc_size: options.get_flag(options::size::ALLOCATION_SIZE),
file_size_block_size,
block_size,
width,
quoting_style,
locale_quoting,
indicator_style,
time_format_recent,
time_format_older,
context,
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
selinux_supported: uucore::selinux::is_selinux_enabled(),
#[cfg(all(feature = "smack", target_os = "linux"))]
smack_supported: uucore::smack::is_smack_enabled(),
group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST),
line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)),
dired,
hyperlink,
tab_size: tab_size.unwrap_or(SPACES_IN_TAB),
})
}
}
fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option<String>), LsError> {
const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y"));
#[expect(clippy::unnecessary_wraps, reason = "internal result helper")]
fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option<String>), LsError> {
Ok((recent.to_string(), older.map(String::from)))
}
if let Some(field) = options
.get_one::<String>(options::TIME_STYLE)
.map(Cow::from)
.or_else(|| std::env::var("TIME_STYLE").ok().map(Cow::from))
{
if options.get_flag(options::FULL_TIME)
&& options.indices_of(options::FULL_TIME).unwrap().next_back()
> options.indices_of(options::TIME_STYLE).unwrap().next_back()
{
ok((format::FULL_ISO, None))
} else {
let field = if let Some(field) = field.strip_prefix("posix-") {
if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX"))
|| std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX"))
{
return ok(LOCALE_FORMAT);
}
field
} else {
&field
};
match field {
"full-iso" => ok((format::FULL_ISO, None)),
"long-iso" => ok((format::LONG_ISO, None)),
"iso" => Ok((
"%m-%d %H:%M".to_string(),
Some(format::ISO.to_string() + " "),
)),
"locale" => ok(LOCALE_FORMAT),
_ if field.starts_with('+') => {
let mut it = field[1..].split('\n');
let recent = it.next().unwrap_or_default();
let older = it.next();
match it.next() {
None => ok((recent, older)),
Some(_) => Err(LsError::TimeStyleParseError(String::from(field))),
}
}
_ => Err(LsError::TimeStyleParseError(String::from(field))),
}
}
} else if options.get_flag(options::FULL_TIME) {
ok((format::FULL_ISO, None))
} else {
ok(LOCALE_FORMAT)
}
}