mirror of
https://github.com/uutils/coreutils
synced 2024-07-21 18:04:45 +00:00
ls: implement --dired
* Support ls --dired * stat-failed.sh: update of the test - we have a small difference * ls --dired: address some of the comments * fix warnings * use unwrap() * Improve test Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> * Simplify test Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> * Remove a word from the spell ignore Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> * remove duplication of the spell ignore Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> * rustfmt --------- Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
This commit is contained in:
parent
bc8e3818a4
commit
9b4d2c6bc4
178
src/uu/ls/src/dired.rs
Normal file
178
src/uu/ls/src/dired.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore dired subdired
|
||||
|
||||
use crate::Config;
|
||||
use std::fmt;
|
||||
use std::io::{BufWriter, Stdout, Write};
|
||||
use uucore::error::UResult;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BytePosition {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
/// Represents the output structure for DIRED, containing positions for both DIRED and SUBDIRED.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiredOutput {
|
||||
pub dired_positions: Vec<BytePosition>,
|
||||
pub subdired_positions: Vec<BytePosition>,
|
||||
pub just_printed_total: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for BytePosition {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} {}", self.start, self.end)
|
||||
}
|
||||
}
|
||||
|
||||
// When --dired is used, all lines starts with 2 spaces
|
||||
static DIRED_TRAILING_OFFSET: usize = 2;
|
||||
|
||||
/// Calculates the byte positions for DIRED
|
||||
pub fn calculate_dired_byte_positions(
|
||||
output_display_len: usize,
|
||||
dfn_len: usize,
|
||||
dired_positions: &[BytePosition],
|
||||
) -> (usize, usize) {
|
||||
let offset_from_previous_line = if let Some(last_position) = dired_positions.last() {
|
||||
last_position.end + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let start = output_display_len + offset_from_previous_line;
|
||||
let end = start + dfn_len;
|
||||
(start, end)
|
||||
}
|
||||
|
||||
pub fn indent(out: &mut BufWriter<Stdout>) -> UResult<()> {
|
||||
write!(out, " ")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn calculate_offset_and_push(dired: &mut DiredOutput, path_len: usize) {
|
||||
let offset = if dired.subdired_positions.is_empty() {
|
||||
DIRED_TRAILING_OFFSET
|
||||
} else {
|
||||
dired.subdired_positions[dired.subdired_positions.len() - 1].start + DIRED_TRAILING_OFFSET
|
||||
};
|
||||
dired.subdired_positions.push(BytePosition {
|
||||
start: offset,
|
||||
end: path_len + offset,
|
||||
});
|
||||
}
|
||||
|
||||
/// Prints the dired output based on the given configuration and dired structure.
|
||||
pub fn print_dired_output(
|
||||
config: &Config,
|
||||
dired: &DiredOutput,
|
||||
out: &mut BufWriter<Stdout>,
|
||||
) -> UResult<()> {
|
||||
out.flush()?;
|
||||
if config.recursive {
|
||||
print_positions("//SUBDIRED//", &dired.subdired_positions);
|
||||
} else if !dired.just_printed_total {
|
||||
print_positions("//DIRED//", &dired.dired_positions);
|
||||
}
|
||||
println!("//DIRED-OPTIONS// --quoting-style={}", config.quoting_style);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to print positions with a given prefix.
|
||||
fn print_positions(prefix: &str, positions: &Vec<BytePosition>) {
|
||||
print!("{}", prefix);
|
||||
for c in positions {
|
||||
print!(" {}", c);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn add_total(total_len: usize, dired: &mut DiredOutput) {
|
||||
dired.just_printed_total = true;
|
||||
dired.dired_positions.push(BytePosition {
|
||||
start: 0,
|
||||
// the 2 is from the trailing spaces
|
||||
// the 1 is from the line ending (\n)
|
||||
end: total_len + DIRED_TRAILING_OFFSET - 1,
|
||||
});
|
||||
}
|
||||
|
||||
/// Calculates byte positions and updates the dired structure.
|
||||
pub fn calculate_and_update_positions(
|
||||
output_display_len: usize,
|
||||
dfn_len: usize,
|
||||
dired: &mut DiredOutput,
|
||||
) {
|
||||
let offset = dired
|
||||
.dired_positions
|
||||
.last()
|
||||
.map_or(DIRED_TRAILING_OFFSET, |last_position| {
|
||||
last_position.start + DIRED_TRAILING_OFFSET
|
||||
});
|
||||
let start = output_display_len + offset + DIRED_TRAILING_OFFSET;
|
||||
let end = start + dfn_len;
|
||||
update_positions(start, end, dired, true);
|
||||
}
|
||||
|
||||
/// Updates the dired positions based on the given start and end positions.
|
||||
/// update when it is the first element in the list (to manage "total X"
|
||||
/// insert when it isn't the about total
|
||||
pub fn update_positions(start: usize, end: usize, dired: &mut DiredOutput, adjust: bool) {
|
||||
if dired.just_printed_total {
|
||||
if let Some(last_position) = dired.dired_positions.last_mut() {
|
||||
*last_position = BytePosition {
|
||||
start: if adjust {
|
||||
start + last_position.end
|
||||
} else {
|
||||
start
|
||||
},
|
||||
end: if adjust { end + last_position.end } else { end },
|
||||
};
|
||||
dired.just_printed_total = false;
|
||||
}
|
||||
} else {
|
||||
dired.dired_positions.push(BytePosition { start, end });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_calculate_dired_byte_positions() {
|
||||
let output_display = "sample_output".to_string();
|
||||
let dfn = "sample_file".to_string();
|
||||
let dired_positions = vec![BytePosition { start: 5, end: 10 }];
|
||||
let (start, end) =
|
||||
calculate_dired_byte_positions(output_display.len(), dfn.len(), &dired_positions);
|
||||
|
||||
assert_eq!(start, 24);
|
||||
assert_eq!(end, 35);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dired_update_positions() {
|
||||
let mut dired = DiredOutput {
|
||||
dired_positions: vec![BytePosition { start: 5, end: 10 }],
|
||||
subdired_positions: vec![],
|
||||
just_printed_total: true,
|
||||
};
|
||||
|
||||
// Test with adjust = true
|
||||
update_positions(15, 20, &mut dired, true);
|
||||
let last_position = dired.dired_positions.last().unwrap();
|
||||
assert_eq!(last_position.start, 25); // 15 + 10 (end of the previous position)
|
||||
assert_eq!(last_position.end, 30); // 20 + 10 (end of the previous position)
|
||||
|
||||
// Test with adjust = false
|
||||
update_positions(30, 35, &mut dired, false);
|
||||
let last_position = dired.dired_positions.last().unwrap();
|
||||
assert_eq!(last_position.start, 30);
|
||||
assert_eq!(last_position.end, 35);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf tabsize dired
|
||||
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf tabsize dired subdired
|
||||
|
||||
use clap::{
|
||||
builder::{NonEmptyStringValueParser, ValueParser},
|
||||
|
@ -61,7 +61,8 @@ use uucore::{
|
|||
version_cmp::version_cmp,
|
||||
};
|
||||
use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning};
|
||||
|
||||
mod dired;
|
||||
use dired::DiredOutput;
|
||||
#[cfg(not(feature = "selinux"))]
|
||||
static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)";
|
||||
#[cfg(feature = "selinux")]
|
||||
|
@ -167,6 +168,7 @@ enum LsError {
|
|||
IOError(std::io::Error),
|
||||
IOErrorContext(std::io::Error, PathBuf, bool),
|
||||
BlockSizeParseError(String),
|
||||
ConflictingArgumentDired(),
|
||||
AlreadyListedError(PathBuf),
|
||||
TimeStyleParseError(String, Vec<String>),
|
||||
}
|
||||
|
@ -179,6 +181,7 @@ impl UError for LsError {
|
|||
Self::IOErrorContext(_, _, false) => 1,
|
||||
Self::IOErrorContext(_, _, true) => 2,
|
||||
Self::BlockSizeParseError(_) => 1,
|
||||
Self::ConflictingArgumentDired() => 1,
|
||||
Self::AlreadyListedError(_) => 2,
|
||||
Self::TimeStyleParseError(_, _) => 1,
|
||||
}
|
||||
|
@ -193,6 +196,10 @@ impl Display for LsError {
|
|||
Self::BlockSizeParseError(s) => {
|
||||
write!(f, "invalid --block-size argument {}", s.quote())
|
||||
}
|
||||
Self::ConflictingArgumentDired() => {
|
||||
write!(f, "--dired requires --format=long")
|
||||
}
|
||||
|
||||
Self::TimeStyleParseError(s, possible_time_styles) => {
|
||||
write!(
|
||||
f,
|
||||
|
@ -406,6 +413,7 @@ pub struct Config {
|
|||
selinux_supported: bool,
|
||||
group_directories_first: bool,
|
||||
line_ending: LineEnding,
|
||||
dired: bool,
|
||||
}
|
||||
|
||||
// Fields that can be removed or added to the long format
|
||||
|
@ -610,6 +618,8 @@ fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> Quot
|
|||
QuotingStyle::C {
|
||||
quotes: quoting_style::Quotes::Double,
|
||||
}
|
||||
} else if options.get_flag(options::DIRED) {
|
||||
QuotingStyle::Literal { show_control }
|
||||
} else {
|
||||
// TODO: use environment variable if available
|
||||
QuotingStyle::Shell {
|
||||
|
@ -954,6 +964,11 @@ impl Config {
|
|||
None
|
||||
};
|
||||
|
||||
let dired = options.get_flag(options::DIRED);
|
||||
if dired && format != Format::Long {
|
||||
return Err(Box::new(LsError::ConflictingArgumentDired()));
|
||||
}
|
||||
|
||||
let dereference = if options.get_flag(options::dereference::ALL) {
|
||||
Dereference::All
|
||||
} else if options.get_flag(options::dereference::ARGS) {
|
||||
|
@ -1003,6 +1018,7 @@ impl Config {
|
|||
},
|
||||
group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST),
|
||||
line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)),
|
||||
dired,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1135,7 +1151,7 @@ pub fn uu_app() -> Command {
|
|||
Arg::new(options::DIRED)
|
||||
.long(options::DIRED)
|
||||
.short('D')
|
||||
.hide(true)
|
||||
.help("generate output designed for Emacs' dired (Directory Editor) mode")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
// The next four arguments do not override with the other format
|
||||
|
@ -1844,6 +1860,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
|
|||
let mut files = Vec::<PathData>::new();
|
||||
let mut dirs = Vec::<PathData>::new();
|
||||
let mut out = BufWriter::new(stdout());
|
||||
let mut dired = DiredOutput::default();
|
||||
let initial_locs_len = locs.len();
|
||||
|
||||
for loc in locs {
|
||||
|
@ -1877,7 +1894,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
|
|||
sort_entries(&mut files, config, &mut out);
|
||||
sort_entries(&mut dirs, config, &mut out);
|
||||
|
||||
display_items(&files, config, &mut out)?;
|
||||
display_items(&files, config, &mut out, &mut dired)?;
|
||||
|
||||
for (pos, path_data) in dirs.iter().enumerate() {
|
||||
// Do read_dir call here to match GNU semantics by printing
|
||||
|
@ -1899,7 +1916,13 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
|
|||
// Print dir heading - name... 'total' comes after error display
|
||||
if initial_locs_len > 1 || config.recursive {
|
||||
if pos.eq(&0usize) && files.is_empty() {
|
||||
if config.dired {
|
||||
dired::indent(&mut out)?;
|
||||
}
|
||||
writeln!(out, "{}:", path_data.p_buf.display())?;
|
||||
if config.dired {
|
||||
dired::calculate_offset_and_push(&mut dired, path_data.display_name.len());
|
||||
}
|
||||
} else {
|
||||
writeln!(out, "\n{}:", path_data.p_buf.display())?;
|
||||
}
|
||||
|
@ -1909,9 +1932,18 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
|
|||
&path_data.p_buf,
|
||||
path_data.must_dereference,
|
||||
)?);
|
||||
enter_directory(path_data, read_dir, config, &mut out, &mut listed_ancestors)?;
|
||||
enter_directory(
|
||||
path_data,
|
||||
read_dir,
|
||||
config,
|
||||
&mut out,
|
||||
&mut listed_ancestors,
|
||||
&mut dired,
|
||||
)?;
|
||||
}
|
||||
if config.dired {
|
||||
dired::print_dired_output(config, &dired, &mut out)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -2022,6 +2054,7 @@ fn enter_directory(
|
|||
config: &Config,
|
||||
out: &mut BufWriter<Stdout>,
|
||||
listed_ancestors: &mut HashSet<FileInformation>,
|
||||
dired: &mut DiredOutput,
|
||||
) -> UResult<()> {
|
||||
// Create vec of entries with initial dot files
|
||||
let mut entries: Vec<PathData> = if config.files == Files::All {
|
||||
|
@ -2067,10 +2100,14 @@ fn enter_directory(
|
|||
|
||||
// Print total after any error display
|
||||
if config.format == Format::Long || config.alloc_size {
|
||||
display_total(&entries, config, out)?;
|
||||
let total = return_total(&entries, config, out)?;
|
||||
write!(out, "{}", total.as_str())?;
|
||||
if config.dired {
|
||||
dired::add_total(total.len(), dired);
|
||||
}
|
||||
}
|
||||
|
||||
display_items(&entries, config, out)?;
|
||||
display_items(&entries, config, out, dired)?;
|
||||
|
||||
if config.recursive {
|
||||
for e in entries
|
||||
|
@ -2095,7 +2132,7 @@ fn enter_directory(
|
|||
.insert(FileInformation::from_path(&e.p_buf, e.must_dereference)?)
|
||||
{
|
||||
writeln!(out, "\n{}:", e.p_buf.display())?;
|
||||
enter_directory(e, rd, config, out, listed_ancestors)?;
|
||||
enter_directory(e, rd, config, out, listed_ancestors, dired)?;
|
||||
listed_ancestors
|
||||
.remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?);
|
||||
} else {
|
||||
|
@ -2154,7 +2191,11 @@ fn pad_right(string: &str, count: usize) -> String {
|
|||
format!("{string:<count$}")
|
||||
}
|
||||
|
||||
fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) -> UResult<()> {
|
||||
fn return_total(
|
||||
items: &[PathData],
|
||||
config: &Config,
|
||||
out: &mut BufWriter<Stdout>,
|
||||
) -> UResult<String> {
|
||||
let mut total_size = 0;
|
||||
for item in items {
|
||||
total_size += item
|
||||
|
@ -2162,13 +2203,14 @@ fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
|
|||
.as_ref()
|
||||
.map_or(0, |md| get_block_size(md, config));
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
if config.dired {
|
||||
dired::indent(out)?;
|
||||
}
|
||||
Ok(format!(
|
||||
"total {}{}",
|
||||
display_size(total_size, config),
|
||||
config.line_ending
|
||||
)?;
|
||||
Ok(())
|
||||
))
|
||||
}
|
||||
|
||||
fn display_additional_leading_info(
|
||||
|
@ -2207,7 +2249,12 @@ fn display_additional_leading_info(
|
|||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) -> UResult<()> {
|
||||
fn display_items(
|
||||
items: &[PathData],
|
||||
config: &Config,
|
||||
out: &mut BufWriter<Stdout>,
|
||||
dired: &mut DiredOutput,
|
||||
) -> UResult<()> {
|
||||
// `-Z`, `--context`:
|
||||
// Display the SELinux security context or '?' if none is found. When used with the `-l`
|
||||
// option, print the security context to the left of the size column.
|
||||
|
@ -2220,6 +2267,7 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
|
|||
if config.inode || config.alloc_size {
|
||||
let more_info =
|
||||
display_additional_leading_info(item, &padding_collection, config, out)?;
|
||||
|
||||
write!(out, "{more_info}")?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
|
@ -2228,7 +2276,7 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
|
|||
display_additional_leading_info(item, &padding_collection, config, out)?;
|
||||
write!(out, "{more_info}")?;
|
||||
}
|
||||
display_item_long(item, &padding_collection, config, out)?;
|
||||
display_item_long(item, &padding_collection, config, out, dired)?;
|
||||
}
|
||||
} else {
|
||||
let mut longest_context_len = 1;
|
||||
|
@ -2402,10 +2450,15 @@ fn display_item_long(
|
|||
padding: &PaddingCollection,
|
||||
config: &Config,
|
||||
out: &mut BufWriter<Stdout>,
|
||||
dired: &mut DiredOutput,
|
||||
) -> UResult<()> {
|
||||
let mut output_display: String = String::new();
|
||||
if config.dired {
|
||||
output_display += " ";
|
||||
}
|
||||
if let Some(md) = item.md(out) {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
"{}{} {}",
|
||||
display_permissions(md, true),
|
||||
if item.security_context.len() > 1 {
|
||||
|
@ -2416,49 +2469,54 @@ fn display_item_long(
|
|||
""
|
||||
},
|
||||
pad_left(&display_symlink_count(md), padding.link_count)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if config.long.owner {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
" {}",
|
||||
pad_right(&display_uname(md, config), padding.uname)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if config.long.group {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
" {}",
|
||||
pad_right(&display_group(md, config), padding.group)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if config.context {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
" {}",
|
||||
pad_right(&item.security_context, padding.context)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Author is only different from owner on GNU/Hurd, so we reuse
|
||||
// the owner, since GNU/Hurd is not currently supported by Rust.
|
||||
if config.long.author {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
" {}",
|
||||
pad_right(&display_uname(md, config), padding.uname)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
match display_len_or_rdev(md, config) {
|
||||
SizeOrDeviceId::Size(size) => {
|
||||
write!(out, " {}", pad_left(&size, padding.size))?;
|
||||
write!(output_display, " {}", pad_left(&size, padding.size)).unwrap();
|
||||
}
|
||||
SizeOrDeviceId::Device(major, minor) => {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
" {}, {}",
|
||||
pad_left(
|
||||
&major,
|
||||
|
@ -2478,19 +2536,23 @@ fn display_item_long(
|
|||
#[cfg(unix)]
|
||||
padding.minor,
|
||||
),
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
|
||||
write!(output_display, " {} ", display_date(md, config)).unwrap();
|
||||
|
||||
write!(
|
||||
out,
|
||||
" {} {}{}",
|
||||
display_date(md, config),
|
||||
displayed_file,
|
||||
config.line_ending
|
||||
)?;
|
||||
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
|
||||
if config.dired {
|
||||
let (start, end) = dired::calculate_dired_byte_positions(
|
||||
output_display.len(),
|
||||
displayed_file.len(),
|
||||
&dired.dired_positions,
|
||||
);
|
||||
dired::update_positions(start, end, dired, false);
|
||||
}
|
||||
write!(output_display, "{}{}", displayed_file, config.line_ending).unwrap();
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
let leading_char = {
|
||||
|
@ -2526,7 +2588,7 @@ fn display_item_long(
|
|||
};
|
||||
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
"{}{} {}",
|
||||
format_args!("{leading_char}?????????"),
|
||||
if item.security_context.len() > 1 {
|
||||
|
@ -2537,41 +2599,53 @@ fn display_item_long(
|
|||
""
|
||||
},
|
||||
pad_left("?", padding.link_count)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if config.long.owner {
|
||||
write!(out, " {}", pad_right("?", padding.uname))?;
|
||||
write!(output_display, " {}", pad_right("?", padding.uname)).unwrap();
|
||||
}
|
||||
|
||||
if config.long.group {
|
||||
write!(out, " {}", pad_right("?", padding.group))?;
|
||||
write!(output_display, " {}", pad_right("?", padding.group)).unwrap();
|
||||
}
|
||||
|
||||
if config.context {
|
||||
write!(
|
||||
out,
|
||||
output_display,
|
||||
" {}",
|
||||
pad_right(&item.security_context, padding.context)
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Author is only different from owner on GNU/Hurd, so we reuse
|
||||
// the owner, since GNU/Hurd is not currently supported by Rust.
|
||||
if config.long.author {
|
||||
write!(out, " {}", pad_right("?", padding.uname))?;
|
||||
write!(output_display, " {}", pad_right("?", padding.uname)).unwrap();
|
||||
}
|
||||
|
||||
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
|
||||
let date_len = 12;
|
||||
|
||||
writeln!(
|
||||
out,
|
||||
" {} {} {}",
|
||||
write!(
|
||||
output_display,
|
||||
" {} {} ",
|
||||
pad_left("?", padding.size),
|
||||
pad_left("?", date_len),
|
||||
displayed_file,
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if config.dired {
|
||||
dired::calculate_and_update_positions(
|
||||
output_display.len(),
|
||||
displayed_file.trim().len(),
|
||||
dired,
|
||||
);
|
||||
}
|
||||
write!(output_display, "{}{}", displayed_file, config.line_ending).unwrap();
|
||||
}
|
||||
write!(out, "{}", output_display)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
// file that was distributed with this source code.
|
||||
use std::char::from_digit;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
|
||||
// These are characters with special meaning in the shell (e.g. bash).
|
||||
// The first const contains characters that only have a special meaning when they appear at the beginning of a name.
|
||||
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#'];
|
||||
const SPECIAL_SHELL_CHARS: &str = "`$&*()|[]{};\\'\"<>?! ";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum QuotingStyle {
|
||||
Shell {
|
||||
escape: bool,
|
||||
|
@ -24,7 +26,7 @@ pub enum QuotingStyle {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Quotes {
|
||||
None,
|
||||
Single,
|
||||
|
@ -316,6 +318,42 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for QuotingStyle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Shell {
|
||||
escape,
|
||||
always_quote,
|
||||
show_control,
|
||||
} => {
|
||||
let mut style = "shell".to_string();
|
||||
if escape {
|
||||
style.push_str("-escape");
|
||||
}
|
||||
if always_quote {
|
||||
style.push_str("-always-quote");
|
||||
}
|
||||
if show_control {
|
||||
style.push_str("-show-control");
|
||||
}
|
||||
f.write_str(&style)
|
||||
}
|
||||
Self::C { .. } => f.write_str("C"),
|
||||
Self::Literal { .. } => f.write_str("literal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Quotes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::None => f.write_str("None"),
|
||||
Self::Single => f.write_str("Single"),
|
||||
Self::Double => f.write_str("Double"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::quoting_style::{escape_name, Quotes, QuotingStyle};
|
||||
|
@ -732,4 +770,45 @@ mod tests {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quoting_style_display() {
|
||||
let style = QuotingStyle::Shell {
|
||||
escape: true,
|
||||
always_quote: false,
|
||||
show_control: false,
|
||||
};
|
||||
assert_eq!(format!("{}", style), "shell-escape");
|
||||
|
||||
let style = QuotingStyle::Shell {
|
||||
escape: false,
|
||||
always_quote: true,
|
||||
show_control: false,
|
||||
};
|
||||
assert_eq!(format!("{}", style), "shell-always-quote");
|
||||
|
||||
let style = QuotingStyle::Shell {
|
||||
escape: false,
|
||||
always_quote: false,
|
||||
show_control: true,
|
||||
};
|
||||
assert_eq!(format!("{}", style), "shell-show-control");
|
||||
|
||||
let style = QuotingStyle::C {
|
||||
quotes: Quotes::Double,
|
||||
};
|
||||
assert_eq!(format!("{}", style), "C");
|
||||
|
||||
let style = QuotingStyle::Literal {
|
||||
show_control: false,
|
||||
};
|
||||
assert_eq!(format!("{}", style), "literal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quotes_display() {
|
||||
assert_eq!(format!("{}", Quotes::None), "None");
|
||||
assert_eq!(format!("{}", Quotes::Single), "Single");
|
||||
assert_eq!(format!("{}", Quotes::Double), "Double");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff
|
||||
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired
|
||||
|
||||
#[cfg(any(unix, feature = "feat_selinux"))]
|
||||
use crate::common::util::expected_result;
|
||||
|
@ -3522,3 +3522,126 @@ fn test_ls_perm_io_errors() {
|
|||
.code_is(1)
|
||||
.stderr_contains("Permission denied");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ls_dired_incompatible() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("--dired")
|
||||
.fails()
|
||||
.code_is(1)
|
||||
.stderr_contains("--dired requires --format=long");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ls_dired_recursive() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("--dired")
|
||||
.arg("-l")
|
||||
.arg("-R")
|
||||
.succeeds()
|
||||
.stdout_does_not_contain("//DIRED//")
|
||||
.stdout_contains(" total 0")
|
||||
.stdout_contains("//SUBDIRED// 2 3")
|
||||
.stdout_contains("//DIRED-OPTIONS// --quoting-style");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ls_dired_simple() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("--dired")
|
||||
.arg("-l")
|
||||
.succeeds()
|
||||
.stdout_contains(" total 0");
|
||||
|
||||
at.mkdir("d");
|
||||
at.touch("d/a1");
|
||||
let mut cmd = scene.ucmd();
|
||||
cmd.arg("--dired").arg("-l").arg("d");
|
||||
let result = cmd.succeeds();
|
||||
result.stdout_contains(" total 0");
|
||||
println!(" result.stdout = {:#?}", result.stdout_str());
|
||||
|
||||
let dired_line = result
|
||||
.stdout_str()
|
||||
.lines()
|
||||
.find(|&line| line.starts_with("//DIRED//"))
|
||||
.unwrap();
|
||||
let positions: Vec<usize> = dired_line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.map(|s| s.parse().unwrap())
|
||||
.collect();
|
||||
|
||||
assert_eq!(positions.len(), 2);
|
||||
|
||||
let start_pos = positions[0];
|
||||
let end_pos = positions[1];
|
||||
|
||||
// Extract the filename using the positions
|
||||
let filename =
|
||||
String::from_utf8(result.stdout_str().as_bytes()[start_pos..end_pos].to_vec()).unwrap();
|
||||
|
||||
assert_eq!(filename, "a1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ls_dired_complex() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
at.mkdir("d");
|
||||
at.mkdir("d/d");
|
||||
at.touch("d/a1");
|
||||
at.touch("d/a22");
|
||||
at.touch("d/a333");
|
||||
at.touch("d/a4444");
|
||||
|
||||
let mut cmd = scene.ucmd();
|
||||
cmd.arg("--dired").arg("-l").arg("d");
|
||||
let result = cmd.succeeds();
|
||||
// Number of blocks
|
||||
#[cfg(target_os = "linux")]
|
||||
result.stdout_contains(" total 4");
|
||||
|
||||
let output = result.stdout_str().to_string();
|
||||
println!("Output:\n{}", output);
|
||||
|
||||
let dired_line = output
|
||||
.lines()
|
||||
.find(|&line| line.starts_with("//DIRED//"))
|
||||
.unwrap();
|
||||
let positions: Vec<usize> = dired_line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.map(|s| s.parse().unwrap())
|
||||
.collect();
|
||||
println!("{:?}", positions);
|
||||
println!("Parsed byte positions: {:?}", positions);
|
||||
assert_eq!(positions.len() % 2, 0); // Ensure there's an even number of positions
|
||||
|
||||
let filenames: Vec<String> = positions
|
||||
.chunks(2)
|
||||
.map(|chunk| {
|
||||
let start_pos = chunk[0];
|
||||
let end_pos = chunk[1];
|
||||
let filename = String::from_utf8(output.as_bytes()[start_pos..=end_pos].to_vec())
|
||||
.unwrap()
|
||||
.trim()
|
||||
.to_string();
|
||||
println!("Extracted filename: {}", filename);
|
||||
filename
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("Extracted filenames: {:?}", filenames);
|
||||
assert_eq!(filenames, vec!["a1", "a22", "a333", "a4444", "d"]);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile for *uutils* build; may be supplied by caller, defaults to 'release'
|
||||
|
||||
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR vdir rcexp xpart
|
||||
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR vdir rcexp xpart dired
|
||||
|
||||
set -e
|
||||
|
||||
|
@ -261,6 +261,10 @@ sed -i -e "s/Try 'mv --help' for more information/For more information, try '--h
|
|||
# disable these test cases
|
||||
sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl
|
||||
|
||||
|
||||
# with ls --dired, in case of error, we have a slightly different error position
|
||||
sed -i -e "s|44 45|47 48|" tests/ls/stat-failed.sh
|
||||
|
||||
sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold <SIZE>' but none was supplied/" -e "/Try 'du --help' for more information./d" tests/du/threshold.sh
|
||||
|
||||
# disable two kind of tests:
|
||||
|
|
Loading…
Reference in a new issue