diff --git a/Cargo.toml b/Cargo.toml index 08e9a3bb2..9136b5d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 201ddc7a6..80a7bf328 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -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, + indicator_style: IndicatorStyle, } // Fields that can be removed or added to the long format @@ -227,12 +246,19 @@ 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] - .iter() - .flat_map(|opt| options.indices_of(opt)) - .flatten() - .any(|i| i >= idx) + 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() + .any(|i| i >= idx) { format = Format::Long; } else { @@ -243,7 +269,6 @@ impl Config { } } } - let files = if options.is_present(options::files::ALL) { Files::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,8 +730,57 @@ 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 + // Positional arguments .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)); let matches = app.get_matches_from(args); @@ -1117,15 +1207,24 @@ fn display_file_name( config: &Config, ) -> Cell { let mut name = get_file_name(path, strip); + let file_type = metadata.file_type(); - if config.classify { - let file_type = metadata.file_type(); - if file_type.is_dir() { - name.push('/'); - } else if file_type.is_symlink() { - name.push('@'); + match config.indicator_style { + IndicatorStyle::Classify | IndicatorStyle::FileType => { + if file_type.is_dir() { + name.push('/'); + } + 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,11 +1333,29 @@ 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; } } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 7d5a3da7b..638102cc7 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -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() { diff --git a/tests/common/util.rs b/tests/common/util.rs index a2fab66c6..d33b1943d 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -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",