Implemented --indicator-style flag on ls. (#1907)

* Implemented --indicator-style flag on ls.

* Rust fmt

* Grouped indicator_style args.

* Added tests for sockets and pipes.

Needed to modify util.rs to add support for pipes (aka FIFOs).

* Updated util.rs to remove FIFO operations on Windows

* Fixed slight error in specifying (not(windows))

* Fixed style violations and added indicator_style test for non-unix systems
This commit is contained in:
Ricardo Iglesias 2021-03-29 04:10:13 -07:00 committed by GitHub
parent 8320b1ec5f
commit 5f17719a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 290 additions and 34 deletions

View file

@ -345,11 +345,13 @@ time = "0.1"
unindent = "0.1"
uucore = { version=">=0.0.7", package="uucore", path="src/uucore", features=["entries"] }
walkdir = "2.2"
tempdir = "0.3"
[target.'cfg(unix)'.dev-dependencies]
rust-users = { version="0.10", package="users" }
unix_socket = "0.5.0"
[[bin]]
name = "coreutils"
path = "src/bin/coreutils.rs"

View file

@ -104,6 +104,14 @@ pub mod options {
pub static HUMAN_READABLE: &str = "human-readable";
pub static SI: &str = "si";
}
pub mod indicator_style {
pub static NONE: &str = "none";
pub static SLASH: &str = "slash";
pub static FILE_TYPE: &str = "file-type";
pub static CLASSIFY: &str = "classify";
}
pub static WIDTH: &str = "width";
pub static AUTHOR: &str = "author";
pub static NO_GROUP: &str = "no-group";
@ -113,12 +121,15 @@ pub mod options {
pub static IGNORE_BACKUPS: &str = "ignore-backups";
pub static DIRECTORY: &str = "directory";
pub static CLASSIFY: &str = "classify";
pub static FILE_TYPE: &str = "file-type";
pub static SLASH: &str = "p";
pub static INODE: &str = "inode";
pub static DEREFERENCE: &str = "dereference";
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";
}
#[derive(PartialEq, Eq)]
@ -157,6 +168,14 @@ enum Time {
Change,
}
#[derive(PartialEq, Eq)]
enum IndicatorStyle {
None,
Slash,
FileType,
Classify,
}
struct Config {
format: Format,
files: Files,
@ -164,7 +183,6 @@ struct Config {
recursive: bool,
reverse: bool,
dereference: bool,
classify: bool,
ignore_backups: bool,
size_format: SizeFormat,
directory: bool,
@ -175,6 +193,7 @@ struct Config {
color: bool,
long: LongFormat,
width: Option<u16>,
indicator_style: IndicatorStyle,
}
// Fields that can be removed or added to the long format
@ -227,8 +246,15 @@ impl Config {
// options, but manually whether they have an index that's greater than
// the other format options. If so, we set the appropriate format.
if format != Format::Long {
let idx = 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]
let idx = 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,
]
.iter()
.flat_map(|opt| options.indices_of(opt))
.flatten()
@ -244,7 +270,6 @@ impl Config {
}
}
let files = if options.is_present(options::files::ALL) {
Files::All
} else if options.is_present(options::files::ALMOST_ALL) {
@ -334,6 +359,32 @@ impl Config {
})
.or_else(|| termsize::get().map(|s| s.cols));
let indicator_style = if let Some(field) = options.value_of(options::INDICATOR_STYLE) {
match field {
"none" => IndicatorStyle::None,
"file-type" => IndicatorStyle::FileType,
"classify" => IndicatorStyle::Classify,
"slash" => IndicatorStyle::Slash,
&_ => IndicatorStyle::None,
}
} else if options.is_present(options::indicator_style::NONE) {
IndicatorStyle::None
} else if options.is_present(options::indicator_style::CLASSIFY)
|| options.is_present(options::CLASSIFY)
{
IndicatorStyle::Classify
} else if options.is_present(options::indicator_style::SLASH)
|| options.is_present(options::SLASH)
{
IndicatorStyle::Slash
} else if options.is_present(options::indicator_style::FILE_TYPE)
|| options.is_present(options::FILE_TYPE)
{
IndicatorStyle::FileType
} else {
IndicatorStyle::None
};
Config {
format,
files,
@ -341,7 +392,6 @@ impl Config {
recursive: options.is_present(options::RECURSIVE),
reverse: options.is_present(options::REVERSE),
dereference: options.is_present(options::DEREFERENCE),
classify: options.is_present(options::CLASSIFY),
ignore_backups: options.is_present(options::IGNORE_BACKUPS),
size_format,
directory: options.is_present(options::DIRECTORY),
@ -352,6 +402,7 @@ impl Config {
inode: options.is_present(options::INODE),
long,
width,
indicator_style,
}
}
}
@ -623,15 +674,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
specified.",
),
)
.arg(
Arg::with_name(options::CLASSIFY)
.short("F")
.long(options::CLASSIFY)
.help("Append a character to each file name indicating the file type. Also, for \
regular files that are executable, append '*'. The file type indicators are \
'/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \
'>' for doors, and nothing for regular files.",
))
.arg(
Arg::with_name(options::size::HUMAN_READABLE)
.short("h")
@ -659,7 +701,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
file the link references rather than the link itself.",
),
)
.arg(
Arg::with_name(options::REVERSE)
.short("r")
@ -689,6 +730,55 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.require_equals(true)
.min_values(0),
)
.arg(
Arg::with_name(options::INDICATOR_STYLE)
.long(options::INDICATOR_STYLE)
.help(" append indicator with style WORD to entry names: none (default), slash\
(-p), file-type (--file-type), classify (-F)")
.takes_value(true)
.possible_values(&["none", "slash", "file-type", "classify"])
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::INDICATOR_STYLE,
]))
.arg(
Arg::with_name(options::CLASSIFY)
.short("F")
.long(options::CLASSIFY)
.help("Append a character to each file name indicating the file type. Also, for \
regular files that are executable, append '*'. The file type indicators are \
'/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \
'>' for doors, and nothing for regular files.")
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::INDICATOR_STYLE,
])
)
.arg(
Arg::with_name(options::FILE_TYPE)
.long(options::FILE_TYPE)
.help("Same as --classify, but do not append '*'")
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::INDICATOR_STYLE,
]))
.arg(
Arg::with_name(options::SLASH)
.short(options::SLASH)
.help("Append / indicator to directories."
)
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::INDICATOR_STYLE,
]))
// Positional arguments
.arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true));
@ -1117,15 +1207,24 @@ fn display_file_name(
config: &Config,
) -> Cell {
let mut name = get_file_name(path, strip);
if config.classify {
let file_type = metadata.file_type();
match config.indicator_style {
IndicatorStyle::Classify | IndicatorStyle::FileType => {
if file_type.is_dir() {
name.push('/');
} else if file_type.is_symlink() {
}
if file_type.is_symlink() {
name.push('@');
}
}
IndicatorStyle::Slash => {
if file_type.is_dir() {
name.push('/');
}
}
_ => (),
};
if config.format == Format::Long && metadata.file_type().is_symlink() {
if let Ok(target) = path.read_link() {
@ -1181,8 +1280,7 @@ fn display_file_name(
let mut width = UnicodeWidthStr::width(&*name);
let ext;
if config.color || config.classify {
if config.color || config.indicator_style != IndicatorStyle::None {
let file_type = metadata.file_type();
let (code, sym) = if file_type.is_dir() {
@ -1235,12 +1333,30 @@ fn display_file_name(
if config.color {
name = color_name(name, code);
}
if config.classify {
if let Some(s) = sym {
name.push(s);
width += 1;
let char_opt = match config.indicator_style {
IndicatorStyle::Classify => sym,
IndicatorStyle::FileType => {
// Don't append an asterisk.
match sym {
Some('*') => None,
_ => sym,
}
}
IndicatorStyle::Slash => {
// Append only a slash.
match sym {
Some('/') => Some('/'),
_ => None,
}
}
IndicatorStyle::None => None,
};
if let Some(c) = char_opt {
name.push(c);
width += 1;
}
}
if config.format == Format::Long && metadata.file_type().is_symlink() {

View file

@ -1,3 +1,5 @@
#[cfg(unix)]
extern crate unix_socket;
use crate::common::util::*;
extern crate regex;
@ -11,7 +13,13 @@ extern crate libc;
#[cfg(not(windows))]
use self::libc::umask;
#[cfg(not(windows))]
use std::path::PathBuf;
#[cfg(not(windows))]
use std::sync::Mutex;
#[cfg(not(windows))]
extern crate tempdir;
#[cfg(not(windows))]
use self::tempdir::TempDir;
#[cfg(not(windows))]
lazy_static! {
@ -813,6 +821,112 @@ fn test_ls_inode() {
assert_eq!(inode_short, inode_long)
}
#[test]
#[cfg(not(windows))]
fn test_ls_indicator_style() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
// Setup: Directory, Symlink, and Pipes.
at.mkdir("directory");
assert!(at.dir_exists("directory"));
at.touch(&at.plus_as_string("link-src"));
at.symlink_file("link-src", "link-dest.link");
assert!(at.is_symlink("link-dest.link"));
at.mkfifo("named-pipe.fifo");
assert!(at.is_fifo("named-pipe.fifo"));
// Classify, File-Type, and Slash all contain indicators for directories.
let options = vec!["classify", "file-type", "slash"];
for opt in options {
// Verify that classify and file-type both contain indicators for symlinks.
let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run();
println!("stdout = {:?}", result.stdout);
assert!(result.stdout.contains("/"));
}
// Same test as above, but with the alternate flags.
let options = vec!["--classify", "--file-type", "-p"];
for opt in options {
let result = scene.ucmd().arg(format!("{}", opt)).run();
println!("stdout = {:?}", result.stdout);
assert!(result.stdout.contains("/"));
}
// Classify and File-Type all contain indicators for pipes and links.
let options = vec!["classify", "file-type"];
for opt in options {
// Verify that classify and file-type both contain indicators for symlinks.
let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run();
println!("stdout = {}", result.stdout);
assert!(result.stdout.contains("@"));
assert!(result.stdout.contains("|"));
}
// Test sockets. Because the canonical way of making sockets to test is with
// TempDir, we need a separate test.
{
use self::unix_socket::UnixListener;
let dir = TempDir::new("unix_socket").expect("failed to create dir");
let socket_path = dir.path().join("sock");
let _listener = UnixListener::bind(&socket_path).expect("failed to create socket");
new_ucmd!()
.args(&[
PathBuf::from(dir.path().to_str().unwrap()),
PathBuf::from("--indicator-style=classify"),
])
.succeeds()
.stdout_only("sock=\n");
}
}
// Essentially the same test as above, but only test symlinks and directories,
// not pipes or sockets.
#[test]
#[cfg(not(unix))]
fn test_ls_indicator_style() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
// Setup: Directory, Symlink.
at.mkdir("directory");
assert!(at.dir_exists("directory"));
at.touch(&at.plus_as_string("link-src"));
at.symlink_file("link-src", "link-dest.link");
assert!(at.is_symlink("link-dest.link"));
// Classify, File-Type, and Slash all contain indicators for directories.
let options = vec!["classify", "file-type", "slash"];
for opt in options {
// Verify that classify and file-type both contain indicators for symlinks.
let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run();
println!("stdout = {:?}", result.stdout);
assert!(result.stdout.contains("/"));
}
// Same test as above, but with the alternate flags.
let options = vec!["--classify", "--file-type", "-p"];
for opt in options {
let result = scene.ucmd().arg(format!("{}", opt)).run();
println!("stdout = {:?}", result.stdout);
assert!(result.stdout.contains("/"));
}
// Classify and File-Type all contain indicators for pipes and links.
let options = vec!["classify", "file-type"];
for opt in options {
// Verify that classify and file-type both contain indicators for symlinks.
let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run();
println!("stdout = {}", result.stdout);
assert!(result.stdout.contains("@"));
}
}
#[cfg(not(any(target_vendor = "apple", target_os = "windows")))] // Truncate not available on mac or win
#[test]
fn test_ls_human_si() {

View file

@ -1,7 +1,8 @@
#![allow(dead_code)]
use libc;
use std::env;
use std::ffi::OsStr;
use std::ffi::{CString, OsStr};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Result, Write};
#[cfg(unix)]
@ -290,6 +291,29 @@ impl AtPath {
File::create(&self.plus(file)).unwrap();
}
#[cfg(not(windows))]
pub fn mkfifo(&self, fifo: &str) {
let full_path = self.plus_as_string(fifo);
log_info("mkfifo", &full_path);
unsafe {
let fifo_name: CString = CString::new(full_path).expect("CString creation failed.");
libc::mkfifo(fifo_name.as_ptr(), libc::S_IWUSR | libc::S_IRUSR);
}
}
#[cfg(not(windows))]
pub fn is_fifo(&self, fifo: &str) -> bool {
unsafe {
let name = CString::new(self.plus_as_string(fifo)).unwrap();
let mut stat: libc::stat = std::mem::zeroed();
if libc::stat(name.as_ptr(), &mut stat) >= 0 {
libc::S_IFIFO & stat.st_mode != 0
} else {
false
}
}
}
pub fn symlink_file(&self, src: &str, dst: &str) {
log_info(
"symlink",