diff --git a/README.md b/README.md index 43e598f5a..57de8f20c 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ These options are available when running with `--long` (`-l`): - **-t**, **--time=(field)**: which timestamp field to use - **-u**, **--accessed**: use the accessed timestamp field - **-U**, **--created**: use the created timestamp field +- **-X**, **--dereference**: dereference symlinks for file information - **-Z**, **--context**: list each file’s security context - **-@**, **--extended**: list each file’s extended attributes and sizes - **--changed**: use the changed timestamp field diff --git a/completions/fish/eza.fish b/completions/fish/eza.fish index 6ea8c6f23..bd3049dde 100755 --- a/completions/fish/eza.fish +++ b/completions/fish/eza.fish @@ -1,35 +1,35 @@ # Meta-stuff -complete -c eza -s 'v' -l 'version' -d "Show version of eza" -complete -c eza -s '?' -l 'help' -d "Show list of command-line options" +complete -c eza -s v -l version -d "Show version of eza" +complete -c eza -s '?' -l help -d "Show list of command-line options" # Display options -complete -c eza -s '1' -l 'oneline' -d "Display one entry per line" -complete -c eza -s 'l' -l 'long' -d "Display extended file metadata as a table" -complete -c eza -s 'G' -l 'grid' -d "Display entries in a grid" -complete -c eza -s 'x' -l 'across' -d "Sort the grid across, rather than downwards" -complete -c eza -s 'R' -l 'recurse' -d "Recurse into directories" -complete -c eza -s 'T' -l 'tree' -d "Recurse into directories as a tree" -complete -c eza -s 'F' -l 'classify' -d "Display type indicator by file names" -complete -c eza -l 'color' \ - -l 'colour' -d "When to use terminal colours" -x -a " +complete -c eza -s 1 -l oneline -d "Display one entry per line" +complete -c eza -s l -l long -d "Display extended file metadata as a table" +complete -c eza -s G -l grid -d "Display entries in a grid" +complete -c eza -s x -l across -d "Sort the grid across, rather than downwards" +complete -c eza -s R -l recurse -d "Recurse into directories" +complete -c eza -s T -l tree -d "Recurse into directories as a tree" +complete -c eza -s F -l classify -d "Display type indicator by file names" +complete -c eza -l color \ + -l colour -d "When to use terminal colours" -x -a " always\t'Always use colour' auto\t'Use colour if standard output is a terminal' never\t'Never use colour' " -complete -c eza -l 'color-scale' \ - -l 'colour-scale' -d "Highlight levels of file sizes distinctly" -complete -c eza -l 'icons' -d "Display icons" -complete -c eza -l 'no-icons' -d "Don't display icons" -complete -c eza -l 'hyperlink' -d "Display entries as hyperlinks" +complete -c eza -l color-scale \ + -l colour-scale -d "Highlight levels of file sizes distinctly" +complete -c eza -l icons -d "Display icons" +complete -c eza -l no-icons -d "Don't display icons" +complete -c eza -l hyperlink -d "Display entries as hyperlinks" # Filtering and sorting options -complete -c eza -l 'group-directories-first' -d "Sort directories before other files" -complete -c eza -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'" -complete -c eza -s 'a' -l 'all' -d "Show hidden and 'dot' files" -complete -c eza -s 'd' -l 'list-dirs' -d "List directories like regular files" -complete -c eza -s 'L' -l 'level' -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9" -complete -c eza -s 'r' -l 'reverse' -d "Reverse the sort order" -complete -c eza -s 's' -l 'sort' -d "Which field to sort by" -x -a " +complete -c eza -l group-directories-first -d "Sort directories before other files" +complete -c eza -l git-ignore -d "Ignore files mentioned in '.gitignore'" +complete -c eza -s a -l all -d "Show hidden and 'dot' files" +complete -c eza -s d -l list-dirs -d "List directories like regular files" +complete -c eza -s L -l level -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9" +complete -c eza -s r -l reverse -d "Reverse the sort order" +complete -c eza -s s -l sort -d "Which field to sort by" -x -a " accessed\t'Sort by file accessed time' age\t'Sort by file modified time (newest first)' changed\t'Sort by changed time' @@ -53,44 +53,45 @@ complete -c eza -s 's' -l 'sort' -d "Which field to sort by" -x -a " type\t'Sort by file type' " -complete -c eza -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r -complete -c eza -s 'D' -l 'only-dirs' -d "List only directories" +complete -c eza -s I -l ignore-glob -d "Ignore files that match these glob patterns" -r +complete -c eza -s D -l only-dirs -d "List only directories" # Long view options -complete -c eza -s 'b' -l 'binary' -d "List file sizes with binary prefixes" -complete -c eza -s 'B' -l 'bytes' -d "List file sizes in bytes, without any prefixes" -complete -c eza -s 'g' -l 'group' -d "List each file's group" -complete -c eza -s 'h' -l 'header' -d "Add a header row to each column" -complete -c eza -s 'H' -l 'links' -d "List each file's number of hard links" -complete -c eza -s 'i' -l 'inode' -d "List each file's inode number" -complete -c eza -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks" -complete -c eza -s 't' -l 'time' -d "Which timestamp field to list" -x -a " +complete -c eza -s b -l binary -d "List file sizes with binary prefixes" +complete -c eza -s B -l bytes -d "List file sizes in bytes, without any prefixes" +complete -c eza -s g -l group -d "List each file's group" +complete -c eza -s h -l header -d "Add a header row to each column" +complete -c eza -s H -l links -d "List each file's number of hard links" +complete -c eza -s i -l inode -d "List each file's inode number" +complete -c eza -s S -l blocks -d "List each file's number of filesystem blocks" +complete -c eza -s t -l time -d "Which timestamp field to list" -x -a " modified\t'Display modified time' changed\t'Display changed time' accessed\t'Display accessed time' created\t'Display created time' " -complete -c eza -s 'm' -l 'modified' -d "Use the modified timestamp field" -complete -c eza -s 'n' -l 'numeric' -d "List numeric user and group IDs." -complete -c eza -l 'changed' -d "Use the changed timestamp field" -complete -c eza -s 'u' -l 'accessed' -d "Use the accessed timestamp field" -complete -c eza -s 'U' -l 'created' -d "Use the created timestamp field" -complete -c eza -l 'time-style' -d "How to format timestamps" -x -a " +complete -c exa -s X -l dereference -d "dereference symlinks for file information" +complete -c eza -s m -l modified -d "Use the modified timestamp field" +complete -c eza -s n -l numeric -d "List numeric user and group IDs." +complete -c eza -l changed -d "Use the changed timestamp field" +complete -c eza -s u -l accessed -d "Use the accessed timestamp field" +complete -c eza -s U -l created -d "Use the created timestamp field" +complete -c eza -l time-style -d "How to format timestamps" -x -a " default\t'Use the default time style' iso\t'Display brief ISO timestamps' long-iso\t'Display longer ISO timestaps, up to the minute' full-iso\t'Display full ISO timestamps, up to the nanosecond' relative\t'Display relative timestamps' " -complete -c eza -l 'no-permissions' -d "Suppress the permissions field" -complete -c eza -s 'o' -l 'octal-permissions' -d "List each file's permission in octal format" -complete -c eza -l 'no-filesize' -d "Suppress the filesize field" -complete -c eza -l 'no-user' -d "Suppress the user field" -complete -c eza -l 'no-time' -d "Suppress the time field" +complete -c eza -l no-permissions -d "Suppress the permissions field" +complete -c eza -s o -l octal-permissions -d "List each file's permission in octal format" +complete -c eza -l no-filesize -d "Suppress the filesize field" +complete -c eza -l no-user -d "Suppress the user field" +complete -c eza -l no-time -d "Suppress the time field" # Optional extras -complete -c eza -l 'git' -d "List each file's Git status, if tracked" -complete -c eza -l 'git-repos' -d "List each git-repos status and branch name" -complete -c eza -l 'git-repos-no-status' -d "List each git-repos branch name (much faster)" -complete -c eza -s '@' -l 'extended' -d "List each file's extended attributes and sizes" -complete -c eza -s 'Z' -l 'context' -d "List each file's security context" +complete -c eza -l git -d "List each file's Git status, if tracked" +complete -c eza -l git-repos -d "List each git-repos status and branch name" +complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)" +complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes" +complete -c eza -s Z -l context -d "List each file's security context" diff --git a/completions/zsh/_eza b/completions/zsh/_eza index f96e13df0..83aa7fc9e 100644 --- a/completions/zsh/_eza +++ b/completions/zsh/_eza @@ -52,6 +52,7 @@ __eza() { --no-time"[Suppress the time field]" \ {-u,--accessed}"[Use the accessed timestamp field]" \ {-U,--created}"[Use the created timestamp field]" \ + {-X,--dereference}"[dereference symlinks for file information]" \ --git"[List each file's Git status, if tracked]" \ --git-repos"[List each git-repos status and branch name]" \ --git-repos-no-status"[List each git-repos branch name (much faster)]" \ diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 9d4d4f2b3..f2d709319 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -47,7 +47,7 @@ impl Dir { /// Produce an iterator of IO results of trying to read all the files in /// this directory. - pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool) -> Files<'dir, 'ig> { + pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool, deref_links: bool) -> Files<'dir, 'ig> { Files { inner: self.contents.iter(), dir: self, @@ -55,6 +55,7 @@ impl Dir { dots: dots.dots(), git, git_ignoring, + deref_links, } } @@ -89,6 +90,9 @@ pub struct Files<'dir, 'ig> { git: Option<&'ig GitCache>, git_ignoring: bool, + + /// Whether symbolic links should be dereferenced when querying information. + deref_links: bool, } impl<'dir, 'ig> Files<'dir, 'ig> { @@ -125,7 +129,7 @@ impl<'dir, 'ig> Files<'dir, 'ig> { } } - return Some(File::from_args(path.clone(), self.dir, filename) + return Some(File::from_args(path.clone(), self.dir, filename, self.deref_links) .map_err(|e| (path.clone(), e))) } diff --git a/src/fs/file.rs b/src/fs/file.rs index 1696e80d0..54fd8705f 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -69,12 +69,18 @@ pub struct File<'dir> { /// means that they should be skipped when recursing. pub is_all_all: bool, + /// Whether to dereference symbolic links when querying for information. + /// + /// For instance, when querying the size of a symbolic link, if + /// dereferencing is enabled, the size of the target will be displayed + /// instead. + pub deref_links: bool, /// The extended attributes of this file. pub extended_attributes: Vec, } impl<'dir> File<'dir> { - pub fn from_args(path: PathBuf, parent_dir: PD, filename: FN) -> io::Result> + pub fn from_args(path: PathBuf, parent_dir: PD, filename: FN, deref_links: bool) -> io::Result> where PD: Into>, FN: Into> { @@ -87,7 +93,7 @@ impl<'dir> File<'dir> { let is_all_all = false; let extended_attributes = File::gather_extended_attributes(&path); - Ok(File { name, ext, path, metadata, parent_dir, is_all_all, extended_attributes }) + Ok(File { name, ext, path, metadata, parent_dir, is_all_all, deref_links, extended_attributes }) } pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result> { @@ -100,7 +106,7 @@ impl<'dir> File<'dir> { let parent_dir = Some(parent_dir); let extended_attributes = File::gather_extended_attributes(&path); - Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, extended_attributes }) + Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, deref_links: false, extended_attributes }) } pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result> { @@ -112,7 +118,7 @@ impl<'dir> File<'dir> { let parent_dir = Some(parent_dir); let extended_attributes = File::gather_extended_attributes(&path); - Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, extended_attributes }) + Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, deref_links: false, extended_attributes }) } /// A file’s name is derived from its string. This needs to handle directories @@ -285,7 +291,7 @@ impl<'dir> File<'dir> { let ext = File::ext(&path); let name = File::filename(&path); let extended_attributes = File::gather_extended_attributes(&absolute_path); - let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, extended_attributes }; + let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, deref_links: self.deref_links, extended_attributes }; FileTarget::Ok(Box::new(file)) } Err(e) => { @@ -295,6 +301,28 @@ impl<'dir> File<'dir> { } } + /// Assuming this file is a symlink, follows that link and any further + /// links recursively, returning the result from following the trail. + /// + /// For a working symlink that the user is allowed to follow, + /// this will be the `File` object at the other end, which can then have + /// its name, colour, and other details read. + /// + /// For a broken symlink, returns where the file *would* be, if it + /// existed. If this file cannot be read at all, returns the error that + /// we got when we tried to read it. + pub fn link_target_recurse(&self) -> FileTarget<'dir> { + let target = self.link_target(); + if let FileTarget::Ok(f) = target { + if f.is_link() { + return f.link_target_recurse(); + } else { + return FileTarget::Ok(f); + } + } + target + } + /// This file’s number of hard links. /// /// It also reports whether this is both a regular file, and a file with @@ -331,16 +359,27 @@ impl<'dir> File<'dir> { } } - /// The ID of the user that own this file. - #[cfg(unix)] - pub fn user(&self) -> f::User { - f::User(self.metadata.uid()) + /// The ID of the user that own this file. If dereferencing links, the links + /// may be broken, in which case `None` will be returned. + pub fn user(&self) -> Option { + if self.is_link() && self.deref_links { + match self.link_target_recurse() { + FileTarget::Ok(f) => return f.user(), + _ => return None, + } + } + Some(f::User(self.metadata.uid())) } /// The ID of the group that owns this file. - #[cfg(unix)] - pub fn group(&self) -> f::Group { - f::Group(self.metadata.gid()) + pub fn group(&self) -> Option { + if self.is_link() && self.deref_links { + match self.link_target_recurse() { + FileTarget::Ok(f) => return f.group(), + _ => return None, + } + } + Some(f::Group(self.metadata.gid())) } /// This file’s size, if it’s a regular file. @@ -351,6 +390,10 @@ impl<'dir> File<'dir> { /// /// Block and character devices return their device IDs, because they /// usually just have a file size of zero. + /// + /// Links will return the size of their target (recursively through other + /// links) if dereferencing is enabled, otherwise the size of the link + /// itself. #[cfg(unix)] pub fn size(&self) -> f::Size { if self.is_link() { @@ -374,7 +417,12 @@ impl<'dir> File<'dir> { minor: device_ids[7], }) } - else { + else if self.is_link() && self.deref_links { + match self.link_target() { + FileTarget::Ok(f) => f.size(), + _ => f::Size::None + } + } else { f::Size::Some(self.metadata.len()) } } @@ -391,12 +439,26 @@ impl<'dir> File<'dir> { /// This file’s last modified timestamp, if available on this platform. pub fn modified_time(&self) -> Option { - self.metadata.modified().ok() + if self.is_link() && self.deref_links { + match self.link_target_recurse() { + FileTarget::Ok(f) => f.metadata.modified().ok(), + _ => None, + } + } else { + self.metadata.modified().ok() + } } /// This file’s last changed timestamp, if available on this platform. #[cfg(unix)] pub fn changed_time(&self) -> Option { + if self.is_link() && self.deref_links { + match self.link_target_recurse() { + FileTarget::Ok(f) => return f.changed_time(), + _ => return None, + } + } + let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec()); if sec < 0 { @@ -421,12 +483,26 @@ impl<'dir> File<'dir> { /// This file’s last accessed timestamp, if available on this platform. pub fn accessed_time(&self) -> Option { - self.metadata.accessed().ok() + if self.is_link() && self.deref_links { + match self.link_target_recurse() { + FileTarget::Ok(f) => f.metadata.accessed().ok(), + _ => None, + } + } else { + self.metadata.accessed().ok() + } } /// This file’s created timestamp, if available on this platform. pub fn created_time(&self) -> Option { - self.metadata.created().ok() + if self.is_link() && self.deref_links { + match self.link_target_recurse() { + FileTarget::Ok(f) => f.metadata.created().ok(), + _ => None, + } + } else { + self.metadata.created().ok() + } } /// This file’s ‘type’. @@ -477,11 +553,20 @@ impl<'dir> File<'dir> { /// This file’s permissions, with flags for each bit. #[cfg(unix)] - pub fn permissions(&self) -> f::Permissions { + pub fn permissions(&self) -> Option { + if self.is_link() && self.deref_links { + // If the chain of links is broken, we instead fall through and + // return the permissions of the original link, as would have been + // done if we were not dereferencing. + match self.link_target_recurse() { + FileTarget::Ok(f) => return f.permissions(), + _ => return None, + } + } let bits = self.metadata.mode(); let has_bit = |bit| bits & bit == bit; - f::Permissions { + Some(f::Permissions { user_read: has_bit(modes::USER_READ), user_write: has_bit(modes::USER_WRITE), user_execute: has_bit(modes::USER_EXECUTE), @@ -497,7 +582,7 @@ impl<'dir> File<'dir> { sticky: has_bit(modes::STICKY), setgid: has_bit(modes::SETGID), setuid: has_bit(modes::SETUID), - } + }) } #[cfg(windows)] diff --git a/src/main.rs b/src/main.rs index db09619f5..322f7b9b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,7 +177,7 @@ impl<'args> Exa<'args> { let mut exit_status = 0; for file_path in &self.input_paths { - match File::from_args(PathBuf::from(file_path), None, None) { + match File::from_args(PathBuf::from(file_path), None, None, self.options.view.deref_links) { Err(e) => { exit_status = 2; writeln!(io::stderr(), "{:?}: {}", file_path, e)?; @@ -230,7 +230,7 @@ impl<'args> Exa<'args> { let mut children = Vec::new(); let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore; - for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore) { + for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore, self.options.view.deref_links) { match file { Ok(file) => children.push(file), Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?, diff --git a/src/options/flags.rs b/src/options/flags.rs index 8210867b3..4adbac4ef 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -6,13 +6,14 @@ pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden }; // display options -pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden }; -pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }; -pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden }; -pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden }; -pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden }; -pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden }; -pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; +pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden }; +pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }; +pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden }; +pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden }; +pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden }; +pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden }; +pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; +pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden }; pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) }; pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) }; @@ -75,7 +76,7 @@ pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", pub static ALL_ARGS: Args = Args(&[ &VERSION, &HELP, - &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, + &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS, &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST, diff --git a/src/options/view.rs b/src/options/view.rs index a3e756731..f66f80c3c 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -13,7 +13,8 @@ impl View { let mode = Mode::deduce(matches, vars)?; let width = TerminalWidth::deduce(vars)?; let file_style = FileStyle::deduce(matches, vars)?; - Ok(Self { mode, width, file_style }) + let deref_links = matches.has(&flags::DEREF_LINKS)?; + Ok(Self { mode, width, file_style, deref_links }) } } diff --git a/src/output/details.rs b/src/output/details.rs index c278dccc6..97ad4f20e 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -300,7 +300,7 @@ impl<'a> Render<'a> { rows.push(row); if let Some(ref dir) = egg.dir { - for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring) { + for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring, egg.file.deref_links) { match file_to_add { Ok(f) => { files.push(f); diff --git a/src/output/mod.rs b/src/output/mod.rs index 331d2fdf0..35f2a0539 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -22,6 +22,7 @@ pub struct View { pub mode: Mode, pub width: TerminalWidth, pub file_style: file_name::Options, + pub deref_links: bool, } diff --git a/src/output/render/groups.rs b/src/output/render/groups.rs index ce33f9fad..a250a9fa8 100644 --- a/src/output/render/groups.rs +++ b/src/output/render/groups.rs @@ -5,18 +5,25 @@ use crate::fs::fields as f; use crate::output::cell::TextCell; use crate::output::table::UserFormat; +pub trait Render{ + fn render(self, colours: &C, users: &U, format: UserFormat) -> TextCell; +} -impl f::Group { - pub fn render(self, colours: &C, users: &U, format: UserFormat) -> TextCell { +impl Render for Option { + fn render(self, colours: &C, users: &U, format: UserFormat) -> TextCell { use users::os::unix::GroupExt; let mut style = colours.not_yours(); - let group = match users.get_group_by_gid(self.0) { - Some(g) => (*g).clone(), - None => return TextCell::paint(style, self.0.to_string()), + let group = match self { + Some(g) => match users.get_group_by_gid(g.0) { + Some(g) => (*g).clone(), + None => return TextCell::paint(style, g.0.to_string()), + }, + None => return TextCell::blank(colours.no_group()), }; + let current_uid = users.get_current_uid(); if let Some(current_user) = users.get_user_by_uid(current_uid) { @@ -40,13 +47,14 @@ impl f::Group { pub trait Colours { fn yours(&self) -> Style; fn not_yours(&self) -> Style; + fn no_group(&self) -> Style; } #[cfg(test)] #[allow(unused_results)] pub mod test { - use super::Colours; + use super::{Colours, Render}; use crate::fs::fields as f; use crate::output::cell::TextCell; use crate::output::table::UserFormat; @@ -63,6 +71,7 @@ pub mod test { impl Colours for TestColours { fn yours(&self) -> Style { Fixed(80).normal() } fn not_yours(&self) -> Style { Fixed(81).normal() } + fn no_group(&self) -> Style { Black.italic() } } @@ -71,7 +80,7 @@ pub mod test { let mut users = MockUsers::with_current_uid(1000); users.add_group(Group::new(100, "folk")); - let group = f::Group(100); + let group = Some(f::Group(100)); let expected = TextCell::paint_str(Fixed(81).normal(), "folk"); assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name)); @@ -84,7 +93,7 @@ pub mod test { fn unnamed() { let users = MockUsers::with_current_uid(1000); - let group = f::Group(100); + let group = Some(f::Group(100)); let expected = TextCell::paint_str(Fixed(81).normal(), "100"); assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name)); assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric)); @@ -96,7 +105,7 @@ pub mod test { users.add_user(User::new(2, "eve", 100)); users.add_group(Group::new(100, "folk")); - let group = f::Group(100); + let group = Some(f::Group(100)); let expected = TextCell::paint_str(Fixed(80).normal(), "folk"); assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name)) } @@ -109,14 +118,14 @@ pub mod test { let test_group = Group::new(100, "folk").add_member("eve"); users.add_group(test_group); - let group = f::Group(100); + let group = Some(f::Group(100)); let expected = TextCell::paint_str(Fixed(80).normal(), "folk"); assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name)) } #[test] fn overflow() { - let group = f::Group(2_147_483_648); + let group = Some(f::Group(2_147_483_648)); let expected = TextCell::paint_str(Fixed(81).normal(), "2147483648"); assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric)); } diff --git a/src/output/render/mod.rs b/src/output/render/mod.rs index 5f2809a9f..ac77a1321 100644 --- a/src/output/render/mod.rs +++ b/src/output/render/mod.rs @@ -10,7 +10,7 @@ pub use self::git::Colours as GitColours; #[cfg(unix)] mod groups; #[cfg(unix)] -pub use self::groups::Colours as GroupColours; +pub use self::groups::{Colours as GroupColours, Render as GroupRender}; mod inode; // inode uses just one colour @@ -19,7 +19,7 @@ mod links; pub use self::links::Colours as LinksColours; mod permissions; -pub use self::permissions::Colours as PermissionsColours; +pub use self::permissions::{Colours as PermissionsColours, PermissionsPlusRender}; mod size; pub use self::size::Colours as SizeColours; @@ -32,8 +32,10 @@ pub use self::times::Render as TimeRender; mod users; #[cfg(unix)] pub use self::users::Colours as UserColours; +pub use self::users::Render as UserRender; mod octal; +pub use self::octal::Render as OctalPermissionsRender; // octal uses just one colour mod securityctx; diff --git a/src/output/render/octal.rs b/src/output/render/octal.rs index 5bb64b5db..747465975 100644 --- a/src/output/render/octal.rs +++ b/src/output/render/octal.rs @@ -3,26 +3,37 @@ use ansi_term::Style; use crate::fs::fields as f; use crate::output::cell::TextCell; +pub trait Render { + fn render(&self, style: Style) -> TextCell; +} + +impl Render for Option { + fn render(&self, style: Style) -> TextCell { + match self { + Some(p) => { + let perm = &p.permissions; + let octal_sticky = f::OctalPermissions::bits_to_octal(perm.setuid, perm.setgid, perm.sticky); + let octal_owner = f::OctalPermissions::bits_to_octal(perm.user_read, perm.user_write, perm.user_execute); + let octal_group = f::OctalPermissions::bits_to_octal(perm.group_read, perm.group_write, perm.group_execute); + let octal_other = f::OctalPermissions::bits_to_octal(perm.other_read, perm.other_write, perm.other_execute); + + TextCell::paint(style, format!("{}{}{}{}", octal_sticky, octal_owner, octal_group, octal_other)) + }, + None => TextCell::paint(style, "----".into()) + } + } +} impl f::OctalPermissions { fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 { u8::from(r) * 4 + u8::from(w) * 2 + u8::from(x) } - - pub fn render(&self, style: Style) -> TextCell { - let perm = &self.permissions; - let octal_sticky = Self::bits_to_octal(perm.setuid, perm.setgid, perm.sticky); - let octal_owner = Self::bits_to_octal(perm.user_read, perm.user_write, perm.user_execute); - let octal_group = Self::bits_to_octal(perm.group_read, perm.group_write, perm.group_execute); - let octal_other = Self::bits_to_octal(perm.other_read, perm.other_write, perm.other_execute); - - TextCell::paint(style, format!("{}{}{}{}", octal_sticky, octal_owner, octal_group, octal_other)) - } } #[cfg(test)] pub mod test { + use super::Render; use crate::output::cell::TextCell; use crate::fs::fields as f; @@ -37,7 +48,7 @@ pub mod test { other_read: true, other_write: false, other_execute: true, sticky: false, }; - let octal = f::OctalPermissions{ permissions: bits }; + let octal = Some(f::OctalPermissions{ permissions: bits }); let expected = TextCell::paint_str(Purple.bold(), "0755"); assert_eq!(expected, octal.render(Purple.bold())); @@ -51,7 +62,7 @@ pub mod test { other_read: true, other_write: false, other_execute: false, sticky: false, }; - let octal = f::OctalPermissions{ permissions: bits }; + let octal = Some(f::OctalPermissions{ permissions: bits }); let expected = TextCell::paint_str(Purple.bold(), "0644"); assert_eq!(expected, octal.render(Purple.bold())); @@ -65,7 +76,7 @@ pub mod test { other_read: false, other_write: false, other_execute: false, sticky: false, }; - let octal = f::OctalPermissions{ permissions: bits }; + let octal = Some(f::OctalPermissions{ permissions: bits }); let expected = TextCell::paint_str(Purple.bold(), "0600"); assert_eq!(expected, octal.render(Purple.bold())); @@ -79,7 +90,7 @@ pub mod test { other_read: true, other_write: true, other_execute: true, sticky: false, }; - let octal = f::OctalPermissions{ permissions: bits }; + let octal = Some(f::OctalPermissions{ permissions: bits }); let expected = TextCell::paint_str(Purple.bold(), "4777"); assert_eq!(expected, octal.render(Purple.bold())); @@ -94,7 +105,7 @@ pub mod test { other_read: true, other_write: true, other_execute: true, sticky: false, }; - let octal = f::OctalPermissions{ permissions: bits }; + let octal = Some(f::OctalPermissions{ permissions: bits }); let expected = TextCell::paint_str(Purple.bold(), "2777"); assert_eq!(expected, octal.render(Purple.bold())); @@ -108,7 +119,7 @@ pub mod test { other_read: true, other_write: true, other_execute: true, sticky: true, }; - let octal = f::OctalPermissions{ permissions: bits }; + let octal = Some(f::OctalPermissions{ permissions: bits }); let expected = TextCell::paint_str(Purple.bold(), "1777"); assert_eq!(expected, octal.render(Purple.bold())); diff --git a/src/output/render/permissions.rs b/src/output/render/permissions.rs index 4d358bcaf..4626ad5ad 100644 --- a/src/output/render/permissions.rs +++ b/src/output/render/permissions.rs @@ -1,26 +1,43 @@ +use std::iter; + use ansi_term::{ANSIString, Style}; use crate::fs::fields as f; use crate::output::cell::{TextCell, DisplayWidth}; use crate::output::render::FiletypeColours; +pub trait PermissionsPlusRender { + fn render(&self, colours: &C) -> TextCell; +} -impl f::PermissionsPlus { - #[cfg(unix)] - pub fn render(&self, colours: &C) -> TextCell { - let mut chars = vec![ self.file_type.render(colours) ]; - chars.extend(self.permissions.render(colours, self.file_type.is_regular_file())); - - if self.xattrs { - chars.push(colours.attribute().paint("@")); - } - - // As these are all ASCII characters, we can guarantee that they’re - // all going to be one character wide, and don’t need to compute the - // cell’s display width. - TextCell { - width: DisplayWidth::from(chars.len()), - contents: chars.into(), +#[cfg(unix)] +impl PermissionsPlusRender for Option { + fn render(&self, colours: &C) -> TextCell { + match self { + Some(p) => { + let mut chars = vec![ p.file_type.render(colours) ]; + let permissions = p.permissions; + chars.extend(Some(permissions).render(colours, p.file_type.is_regular_file())); + + if p.xattrs { + chars.push(colours.attribute().paint("@")); + } + + // As these are all ASCII characters, we can guarantee that they’re + // all going to be one character wide, and don’t need to compute the + // cell’s display width. + TextCell { + width: DisplayWidth::from(chars.len()), + contents: chars.into(), + } + }, + None => { + let chars: Vec<_> = iter::repeat(colours.dash().paint("-")).take(10).collect(); + TextCell { + width: DisplayWidth::from(chars.len()), + contents: chars.into(), + } + } } } @@ -36,28 +53,39 @@ impl f::PermissionsPlus { } } +pub trait RenderPermissions { + fn render(&self, colours: &C, is_regular_file: bool) -> Vec>; +} -impl f::Permissions { - pub fn render(&self, colours: &C, is_regular_file: bool) -> Vec> { - - let bit = |bit, chr: &'static str, style: Style| { - if bit { style.paint(chr) } - else { colours.dash().paint("-") } - }; - - vec![ - bit(self.user_read, "r", colours.user_read()), - bit(self.user_write, "w", colours.user_write()), - self.user_execute_bit(colours, is_regular_file), - bit(self.group_read, "r", colours.group_read()), - bit(self.group_write, "w", colours.group_write()), - self.group_execute_bit(colours), - bit(self.other_read, "r", colours.other_read()), - bit(self.other_write, "w", colours.other_write()), - self.other_execute_bit(colours) - ] +impl RenderPermissions for Option { + fn render(&self, colours: &C, is_regular_file: bool) -> Vec> { + match self { + Some(p) => { + let bit = |bit, chr: &'static str, style: Style| { + if bit { style.paint(chr) } + else { colours.dash().paint("-") } + }; + + vec![ + bit(p.user_read, "r", colours.user_read()), + bit(p.user_write, "w", colours.user_write()), + p.user_execute_bit(colours, is_regular_file), + bit(p.group_read, "r", colours.group_read()), + bit(p.group_write, "w", colours.group_write()), + p.group_execute_bit(colours), + bit(p.other_read, "r", colours.other_read()), + bit(p.other_write, "w", colours.other_write()), + p.other_execute_bit(colours) + ] + }, + None => { + iter::repeat(colours.dash().paint("-")).take(9).collect() + } + } } +} +impl f::Permissions { fn user_execute_bit(&self, colours: &C, is_regular_file: bool) -> ANSIString<'static> { match (self.user_execute, self.setuid, is_regular_file) { (false, false, _) => colours.dash().paint("-"), @@ -143,7 +171,7 @@ pub trait Colours { #[cfg(test)] #[allow(unused_results)] pub mod test { - use super::Colours; + use super::{Colours, RenderPermissions}; use crate::output::cell::TextCellContents; use crate::fs::fields as f; @@ -173,11 +201,11 @@ pub mod test { #[test] fn negate() { - let bits = f::Permissions { + let bits = Some(f::Permissions { user_read: false, user_write: false, user_execute: false, setuid: false, group_read: false, group_write: false, group_execute: false, setgid: false, other_read: false, other_write: false, other_execute: false, sticky: false, - }; + }); let expected = TextCellContents::from(vec![ Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(11).paint("-"), @@ -191,11 +219,11 @@ pub mod test { #[test] fn affirm() { - let bits = f::Permissions { + let bits = Some(f::Permissions { user_read: true, user_write: true, user_execute: true, setuid: false, group_read: true, group_write: true, group_execute: true, setgid: false, other_read: true, other_write: true, other_execute: true, sticky: false, - }; + }); let expected = TextCellContents::from(vec![ Fixed(101).paint("r"), Fixed(102).paint("w"), Fixed(103).paint("x"), @@ -209,11 +237,11 @@ pub mod test { #[test] fn specials() { - let bits = f::Permissions { + let bits = Some(f::Permissions { user_read: false, user_write: false, user_execute: true, setuid: true, group_read: false, group_write: false, group_execute: true, setgid: true, other_read: false, other_write: false, other_execute: true, sticky: true, - }; + }); let expected = TextCellContents::from(vec![ Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(110).paint("s"), @@ -227,11 +255,11 @@ pub mod test { #[test] fn extra_specials() { - let bits = f::Permissions { + let bits = Some(f::Permissions { user_read: false, user_write: false, user_execute: false, setuid: true, group_read: false, group_write: false, group_execute: false, setgid: true, other_read: false, other_write: false, other_execute: false, sticky: true, - }; + }); let expected = TextCellContents::from(vec![ Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("S"), diff --git a/src/output/render/users.rs b/src/output/render/users.rs index 6a99a98e3..96f911726 100644 --- a/src/output/render/users.rs +++ b/src/output/render/users.rs @@ -5,16 +5,23 @@ use crate::fs::fields as f; use crate::output::cell::TextCell; use crate::output::table::UserFormat; +pub trait Render { + fn render(self, colours: &C, users: &U, format: UserFormat) -> TextCell; +} -impl f::User { - pub fn render(self, colours: &C, users: &U, format: UserFormat) -> TextCell { - let user_name = match (format, users.get_user_by_uid(self.0)) { - (_, None) => self.0.to_string(), - (UserFormat::Numeric, _) => self.0.to_string(), +impl Render for Option { + fn render(self, colours: &C, users: &U, format: UserFormat) -> TextCell { + let uid = match self { + Some(u) => u.0, + None => return TextCell::blank(colours.no_user()), + }; + let user_name = match (format, users.get_user_by_uid(uid)) { + (_, None) => uid.to_string(), + (UserFormat::Numeric, _) => uid.to_string(), (UserFormat::Name, Some(user)) => user.name().to_string_lossy().into(), }; - let style = if users.get_current_uid() == self.0 { colours.you() } + let style = if users.get_current_uid() == uid { colours.you() } else { colours.someone_else() }; TextCell::paint(style, user_name) } @@ -24,13 +31,14 @@ impl f::User { pub trait Colours { fn you(&self) -> Style; fn someone_else(&self) -> Style; + fn no_user(&self) -> Style; } #[cfg(test)] #[allow(unused_results)] pub mod test { - use super::Colours; + use super::{Colours, Render}; use crate::fs::fields as f; use crate::output::cell::TextCell; use crate::output::table::UserFormat; @@ -46,6 +54,7 @@ pub mod test { impl Colours for TestColours { fn you(&self) -> Style { Red.bold() } fn someone_else(&self) -> Style { Blue.underline() } + fn no_user(&self) -> Style { Black.italic() } } @@ -54,7 +63,7 @@ pub mod test { let mut users = MockUsers::with_current_uid(1000); users.add_user(User::new(1000, "enoch", 100)); - let user = f::User(1000); + let user = Some(f::User(1000)); let expected = TextCell::paint_str(Red.bold(), "enoch"); assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name)); @@ -66,7 +75,7 @@ pub mod test { fn unnamed() { let users = MockUsers::with_current_uid(1000); - let user = f::User(1000); + let user = Some(f::User(1000)); let expected = TextCell::paint_str(Red.bold(), "1000"); assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name)); assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric)); @@ -77,21 +86,21 @@ pub mod test { let mut users = MockUsers::with_current_uid(0); users.add_user(User::new(1000, "enoch", 100)); - let user = f::User(1000); + let user = Some(f::User(1000)); let expected = TextCell::paint_str(Blue.underline(), "enoch"); assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name)); } #[test] fn different_unnamed() { - let user = f::User(1000); + let user = Some(f::User(1000)); let expected = TextCell::paint_str(Blue.underline(), "1000"); assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric)); } #[test] fn overflow() { - let user = f::User(2_147_483_648); + let user = Some(f::User(2_147_483_648)); let expected = TextCell::paint_str(Blue.underline(), "2147483648"); assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric)); } diff --git a/src/output/table.rs b/src/output/table.rs index b25583174..192a555d1 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -15,11 +15,19 @@ use users::UsersCache; use crate::fs::{File, fields as f}; use crate::fs::feature::git::GitCache; use crate::output::cell::TextCell; -use crate::output::render::TimeRender; +use crate::output::render::{ + GroupRender, + OctalPermissionsRender, + PermissionsPlusRender, + TimeRender, + UserRender +}; use crate::output::time::TimeFormat; use crate::theme::Theme; + + /// Options for displaying a table. #[derive(PartialEq, Eq, Debug)] pub struct Options { @@ -471,21 +479,27 @@ impl<'a, 'f> Table<'a> { self.widths.add_widths(row) } - fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus { - f::PermissionsPlus { - file_type: file.type_char(), - #[cfg(unix)] - permissions: file.permissions(), - #[cfg(windows)] - attributes: file.attributes(), - xattrs, + fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> Option { + match file.permissions() { + Some(p) => Some(f::PermissionsPlus { + file_type: file.type_char(), + #[cfg(unix)] + permissions: p, + #[cfg(windows)] + attributes: file.attributes(), + xattrs + }), + None => None, } } #[cfg(unix)] - fn octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions { - f::OctalPermissions { - permissions: file.permissions(), + fn octal_permissions(&self, file: &File<'_>) -> Option { + match file.permissions() { + Some(p) => Some(f::OctalPermissions { + permissions: p, + }), + None => None, } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index e88145a37..d8fbff4ba 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -233,6 +233,7 @@ impl render::GitColours for Theme { impl render::GroupColours for Theme { fn yours(&self) -> Style { self.ui.users.group_yours } fn not_yours(&self) -> Style { self.ui.users.group_not_yours } + fn no_group(&self) -> Style { self.ui.punctuation } } impl render::LinksColours for Theme { @@ -292,6 +293,7 @@ impl render::SizeColours for Theme { impl render::UserColours for Theme { fn you(&self) -> Style { self.ui.users.user_you } fn someone_else(&self) -> Style { self.ui.users.user_someone_else } + fn no_user(&self) -> Style { self.ui.punctuation } } impl FileNameColours for Theme {