From b6363f3ce127e76fd1053e0fd5bc7ddf3016faa9 Mon Sep 17 00:00:00 2001 From: Kevin DCR Date: Fri, 13 Mar 2020 12:27:04 -0500 Subject: [PATCH] Added new flag '--all/-a' for Ls, also refactor some code (#1483) * Utility function to detect hidden folders. Implemented for Unix and Windows. * Rename function argument. * Revert "Rename function argument." This reverts commit e7ab70f0f0f4efc3938d904571ea91dd491fe270. * Add flag '--all/-a' to Ls * Rename function argument. * Check if flag '--all/-a' is present and path is hidden. Replace match with map_err for glob result. Remove redundancy in stream body. Included comments on new stream body. Replace async_stream::stream with async_stream::try_stream. Minor tweaks to is_empty_dir. Fix and refactor is_hidden_dir. * Fix "implicit" bool coerse * Fixed clippy errors --- crates/nu-cli/src/commands/ls.rs | 2 + crates/nu-cli/src/data/files.rs | 4 +- crates/nu-cli/src/shell/filesystem_shell.rs | 104 +++++++++++++------- crates/nu-cli/src/shell/help_shell.rs | 2 +- crates/nu-cli/src/shell/value_shell.rs | 2 +- crates/nu-parser/src/hir/syntax_shape.rs | 2 +- crates/nu-parser/src/parse/parser.rs | 4 +- 7 files changed, 80 insertions(+), 40 deletions(-) diff --git a/crates/nu-cli/src/commands/ls.rs b/crates/nu-cli/src/commands/ls.rs index b2252bdf23..e8ded5962e 100644 --- a/crates/nu-cli/src/commands/ls.rs +++ b/crates/nu-cli/src/commands/ls.rs @@ -10,6 +10,7 @@ pub struct Ls; #[derive(Deserialize)] pub struct LsArgs { pub path: Option>, + pub all: bool, pub full: bool, #[serde(rename = "short-names")] pub short_names: bool, @@ -29,6 +30,7 @@ impl PerItemCommand for Ls { SyntaxShape::Pattern, "a path to get the directory contents from", ) + .switch("all", "also show hidden files", Some('a')) .switch( "full", "list all available columns for each entry", diff --git a/crates/nu-cli/src/data/files.rs b/crates/nu-cli/src/data/files.rs index 257ab96624..95cbcfd0c9 100644 --- a/crates/nu-cli/src/data/files.rs +++ b/crates/nu-cli/src/data/files.rs @@ -36,13 +36,13 @@ pub(crate) fn dir_entry_dict( metadata: Option<&std::fs::Metadata>, tag: impl Into, full: bool, - name_only: bool, + short_name: bool, with_symlink_targets: bool, ) -> Result { let tag = tag.into(); let mut dict = TaggedDictBuilder::new(&tag); - let name = if name_only { + let name = if short_name { filename.file_name().and_then(|s| s.to_str()) } else { filename.to_str() diff --git a/crates/nu-cli/src/shell/filesystem_shell.rs b/crates/nu-cli/src/shell/filesystem_shell.rs index bfe7cec538..5d3a7980ac 100644 --- a/crates/nu-cli/src/shell/filesystem_shell.rs +++ b/crates/nu-cli/src/shell/filesystem_shell.rs @@ -14,7 +14,7 @@ use nu_parser::ExpandContext; use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue}; use rustyline::completion::FilenameCompleter; use rustyline::hint::{Hinter, HistoryHinter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; use trash as SendToTrash; @@ -93,6 +93,7 @@ impl Shell for FilesystemShell { &self, LsArgs { path, + all, full, short_names, with_symlink_targets, @@ -107,7 +108,7 @@ impl Shell for FilesystemShell { let p_tag = p.tag; let mut p = p.item; if p.is_dir() { - if is_dir_empty(&p) { + if is_empty_dir(&p) { return Ok(OutputStream::empty()); } p.push("*"); @@ -115,7 +116,7 @@ impl Shell for FilesystemShell { (p, p_tag) } None => { - if is_dir_empty(&self.path().into()) { + if is_empty_dir(&self.path()) { return Ok(OutputStream::empty()); } else { (PathBuf::from("./*"), context.name.clone()) @@ -123,11 +124,9 @@ impl Shell for FilesystemShell { } }; - let mut paths = match glob::glob(&path.to_string_lossy()) { - Ok(g) => Ok(g), - Err(e) => Err(ShellError::labeled_error("Glob error", e.msg, &p_tag)), - }? - .peekable(); + let mut paths = glob::glob(&path.to_string_lossy()) + .map_err(|e| ShellError::labeled_error("Glob error", e.to_string(), &p_tag))? + .peekable(); if paths.peek().is_none() { return Err(ShellError::labeled_error( @@ -137,35 +136,53 @@ impl Shell for FilesystemShell { )); } - let stream = async_stream! { + // Generated stream: impl Stream + let stream = async_stream::try_stream! { for path in paths { + // Handle CTRL+C presence if ctrl_c.load(Ordering::SeqCst) { break; } - match path { - Ok(p) => match std::fs::symlink_metadata(&p) { - Ok(m) => { - match dir_entry_dict(&p, Some(&m), name_tag.clone(), full, short_names, with_symlink_targets) { - Ok(d) => yield ReturnSuccess::value(d), - Err(e) => yield Err(e) - } - } - Err(e) => { - match e.kind() { - PermissionDenied => { - match dir_entry_dict(&p, None, name_tag.clone(), full, short_names, with_symlink_targets) { - Ok(d) => yield ReturnSuccess::value(d), - Err(e) => yield Err(e) - } - }, - _ => yield Err(ShellError::from(e)) - } - } - } - Err(e) => yield Err(e.into_error().into()), + + // Map GlobError to ShellError and gracefully try to unwrap the path + let path = path.map_err(|e| ShellError::from(e.into_error()))?; + + // Skip if '--all/-a' flag is present and this path is hidden + if !all && is_hidden_dir(&path) { + continue; } + + // Get metadata from current path, if we don't have enough + // permissions to stat on file don't use any metadata, otherwise + // return the error and gracefully unwrap metadata (which yields + // Option) + let metadata = match std::fs::symlink_metadata(&path) { + Ok(metadata) => Ok(Some(metadata)), + Err(e) => if let PermissionDenied = e.kind() { + Ok(None) + } else { + Err(e) + }, + }?; + + // Build dict entry for this path and possibly using some metadata. + // Map the possible dict entry into a Value, gracefully unwrap it + // with '?' + let entry = dir_entry_dict( + &path, + metadata.as_ref(), + name_tag.clone(), + full, + short_names, + with_symlink_targets + ) + .map(|entry| ReturnSuccess::Value(entry.into()))?; + + // Finally yield the generated entry that was mapped to Value + yield entry; } }; + Ok(stream.to_output_stream()) } @@ -1129,9 +1146,30 @@ impl Shell for FilesystemShell { } } -fn is_dir_empty(d: &PathBuf) -> bool { - match d.read_dir() { - Err(_e) => true, +fn is_empty_dir(dir: impl AsRef) -> bool { + match dir.as_ref().read_dir() { + Err(_) => true, Ok(mut s) => s.next().is_none(), } } + +fn is_hidden_dir(dir: impl AsRef) -> bool { + cfg_if::cfg_if! { + if #[cfg(windows)] { + use std::os::windows::fs::MetadataExt; + + if let Ok(metadata) = dir.as_ref().metadata() { + let attributes = metadata.file_attributes(); + // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + (attributes & 0x2) != 0 + } else { + false + } + } else { + dir.as_ref() + .file_name() + .map(|name| name.to_string_lossy().starts_with('.')) + .unwrap_or(false) + } + } +} diff --git a/crates/nu-cli/src/shell/help_shell.rs b/crates/nu-cli/src/shell/help_shell.rs index 77c104fdb8..d3b45374e0 100644 --- a/crates/nu-cli/src/shell/help_shell.rs +++ b/crates/nu-cli/src/shell/help_shell.rs @@ -162,7 +162,7 @@ impl Shell for HelpShell { cwd.pop(); } else { match target.to_str() { - Some(target) => match target.chars().nth(0) { + Some(target) => match target.chars().next() { Some(x) if x == '/' => cwd = PathBuf::from(target), _ => cwd.push(target), }, diff --git a/crates/nu-cli/src/shell/value_shell.rs b/crates/nu-cli/src/shell/value_shell.rs index 56f53ced96..1f9d0e1382 100644 --- a/crates/nu-cli/src/shell/value_shell.rs +++ b/crates/nu-cli/src/shell/value_shell.rs @@ -144,7 +144,7 @@ impl Shell for ValueShell { cwd = PathBuf::from(&self.last_path); } else { match target.to_str() { - Some(target) => match target.chars().nth(0) { + Some(target) => match target.chars().next() { Some(x) if x == '/' => cwd = PathBuf::from(target), _ => cwd.push(target), }, diff --git a/crates/nu-parser/src/hir/syntax_shape.rs b/crates/nu-parser/src/hir/syntax_shape.rs index ace2f43ff4..8b7194625d 100644 --- a/crates/nu-parser/src/hir/syntax_shape.rs +++ b/crates/nu-parser/src/hir/syntax_shape.rs @@ -137,7 +137,7 @@ pub struct ExpandContext<'context> { impl<'context> ExpandContext<'context> { pub(crate) fn homedir(&self) -> Option<&Path> { - self.homedir.as_ref().map(|h| h.as_path()) + self.homedir.as_deref() } pub(crate) fn source(&self) -> &'context Text { diff --git a/crates/nu-parser/src/parse/parser.rs b/crates/nu-parser/src/parse/parser.rs index 65d212121d..f51781c5fe 100644 --- a/crates/nu-parser/src/parse/parser.rs +++ b/crates/nu-parser/src/parse/parser.rs @@ -372,7 +372,7 @@ fn word<'a, T, U, V>( let (input, _) = start_predicate(input)?; let (input, _) = many0(next_predicate)(input)?; - let next_char = &input.fragment.chars().nth(0); + let next_char = &input.fragment.chars().next(); match next_char { Some('.') => {} @@ -609,7 +609,7 @@ fn tight<'a>( let (input, tail) = opt(alt((many1(range_continuation), many1(dot_member))))(input)?; - let next_char = &input.fragment.chars().nth(0); + let next_char = &input.fragment.chars().next(); if is_boundary(*next_char) { if let Some(tail) = tail {