From b719f8d4eb33851cc015fed44a5dbd548518c352 Mon Sep 17 00:00:00 2001
From: JT <547158+jntrnr@users.noreply.github.com>
Date: Fri, 24 Dec 2021 08:41:29 +1100
Subject: [PATCH] Add missing flags to existing commands (#565)

* Add missing flags to existing commands

* fmt
---
 Cargo.lock                                  |  18 +
 crates/nu-command/Cargo.toml                |   4 +
 crates/nu-command/src/core_commands/do_.rs  |  18 +-
 crates/nu-command/src/core_commands/for_.rs |  51 ++-
 crates/nu-command/src/filesystem/ls.rs      | 426 +++++++++++++++++---
 crates/nu-command/src/viewers/table.rs      |  33 +-
 6 files changed, 462 insertions(+), 88 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 18661d5dd..3e3b69b97 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1730,8 +1730,10 @@ dependencies = [
  "titlecase",
  "toml",
  "trash",
+ "umask",
  "unicode-segmentation",
  "url",
+ "users",
  "uuid",
  "zip",
 ]
@@ -3170,6 +3172,12 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
+[[package]]
+name = "umask"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982efbf70ec4d28f7862062c03dd1a4def601a5079e0faf1edc55f2ad0f6fe46"
+
 [[package]]
 name = "uncased"
 version = "0.9.6"
@@ -3239,6 +3247,16 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "users"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
+dependencies = [
+ "libc",
+ "log",
+]
+
 [[package]]
 name = "utf8-width"
 version = "0.1.5"
diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml
index a003a5596..353ea04e5 100644
--- a/crates/nu-command/Cargo.toml
+++ b/crates/nu-command/Cargo.toml
@@ -68,6 +68,10 @@ sha2 = "0.10.0"
 base64 = "0.13.0"
 num = { version = "0.4.0", optional = true }
 
+[target.'cfg(unix)'.dependencies]
+umask = "1.0.0"
+users = "0.11.0"
+
 [dependencies.polars]
 version = "0.18.0"
 optional = true
diff --git a/crates/nu-command/src/core_commands/do_.rs b/crates/nu-command/src/core_commands/do_.rs
index f85f34746..8c0e77d52 100644
--- a/crates/nu-command/src/core_commands/do_.rs
+++ b/crates/nu-command/src/core_commands/do_.rs
@@ -23,6 +23,11 @@ impl Command for Do {
                 SyntaxShape::Block(Some(vec![])),
                 "the block to run",
             )
+            .switch(
+                "ignore-errors",
+                "ignore errors as the block runs",
+                Some('i'),
+            )
             .rest("rest", SyntaxShape::Any, "the parameter(s) for the block")
             .category(Category::Core)
     }
@@ -37,6 +42,8 @@ impl Command for Do {
         let block: Value = call.req(engine_state, stack, 0)?;
         let block_id = block.as_block()?;
 
+        let ignore_errors = call.has_flag("ignore-errors");
+
         let rest: Vec<Value> = call.rest(engine_state, stack, 1)?;
 
         let block = engine_state.get_block(block_id);
@@ -81,6 +88,15 @@ impl Command for Do {
                 )
             }
         }
-        eval_block(engine_state, &mut stack, block, input)
+        let result = eval_block(engine_state, &mut stack, block, input);
+
+        if ignore_errors {
+            match result {
+                Ok(x) => Ok(x),
+                Err(_) => Ok(PipelineData::new(call.head)),
+            }
+        } else {
+            result
+        }
     }
 }
diff --git a/crates/nu-command/src/core_commands/for_.rs b/crates/nu-command/src/core_commands/for_.rs
index d95908ded..4a0f54f78 100644
--- a/crates/nu-command/src/core_commands/for_.rs
+++ b/crates/nu-command/src/core_commands/for_.rs
@@ -35,6 +35,11 @@ impl Command for For {
                 SyntaxShape::Block(Some(vec![])),
                 "the block to run",
             )
+            .switch(
+                "numbered",
+                "returned a numbered item ($it.index and $it.item)",
+                Some('n'),
+            )
             .creates_scope()
             .category(Category::Core)
     }
@@ -60,6 +65,8 @@ impl Command for For {
             .as_block()
             .expect("internal error: expected block");
 
+        let numbered = call.has_flag("numbered");
+
         let ctrlc = engine_state.ctrlc.clone();
         let engine_state = engine_state.clone();
         let block = engine_state.get_block(block_id).clone();
@@ -68,8 +75,26 @@ impl Command for For {
         match values {
             Value::List { vals, .. } => Ok(vals
                 .into_iter()
-                .map(move |x| {
-                    stack.add_var(var_id, x);
+                .enumerate()
+                .map(move |(idx, x)| {
+                    stack.add_var(
+                        var_id,
+                        if numbered {
+                            Value::Record {
+                                cols: vec!["index".into(), "item".into()],
+                                vals: vec![
+                                    Value::Int {
+                                        val: idx as i64,
+                                        span: head,
+                                    },
+                                    x,
+                                ],
+                                span: head,
+                            }
+                        } else {
+                            x
+                        },
+                    );
 
                     //let block = engine_state.get_block(block_id);
                     match eval_block(&engine_state, &mut stack, &block, PipelineData::new(head)) {
@@ -80,8 +105,26 @@ impl Command for For {
                 .into_pipeline_data(ctrlc)),
             Value::Range { val, .. } => Ok(val
                 .into_range_iter()?
-                .map(move |x| {
-                    stack.add_var(var_id, x);
+                .enumerate()
+                .map(move |(idx, x)| {
+                    stack.add_var(
+                        var_id,
+                        if numbered {
+                            Value::Record {
+                                cols: vec!["index".into(), "item".into()],
+                                vals: vec![
+                                    Value::Int {
+                                        val: idx as i64,
+                                        span: head,
+                                    },
+                                    x,
+                                ],
+                                span: head,
+                            }
+                        } else {
+                            x
+                        },
+                    );
 
                     //let block = engine_state.get_block(block_id);
                     match eval_block(&engine_state, &mut stack, &block, PipelineData::new(head)) {
diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs
index 955d9684b..c8537fd2f 100644
--- a/crates/nu-command/src/filesystem/ls.rs
+++ b/crates/nu-command/src/filesystem/ls.rs
@@ -3,10 +3,14 @@ use nu_engine::eval_expression;
 use nu_protocol::ast::Call;
 use nu_protocol::engine::{Command, EngineState, Stack};
 use nu_protocol::{
-    Category, DataSource, IntoInterruptiblePipelineData, PipelineData, PipelineMetadata, Signature,
-    SyntaxShape, Value,
+    Category, DataSource, IntoInterruptiblePipelineData, PipelineData, PipelineMetadata,
+    ShellError, Signature, Span, SyntaxShape, Value,
 };
 
+use std::io::ErrorKind;
+#[cfg(unix)]
+use std::os::unix::fs::PermissionsExt;
+
 #[derive(Clone)]
 pub struct Ls;
 
@@ -27,6 +31,22 @@ impl Command for Ls {
                 SyntaxShape::GlobPattern,
                 "the glob pattern to use",
             )
+            .switch("all", "Show hidden files", Some('a'))
+            .switch(
+                "long",
+                "List all available columns for each entry",
+                Some('l'),
+            )
+            .switch(
+                "short-names",
+                "Only print the file names and not the path",
+                Some('s'),
+            )
+            .switch(
+                "du",
+                "Display the apparent directory size in place of the directory metadata size",
+                Some('d'),
+            )
             .category(Category::FileSystem)
     }
 
@@ -37,7 +57,13 @@ impl Command for Ls {
         call: &Call,
         _input: PipelineData,
     ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
-        let pattern = if let Some(expr) = call.positional.get(0) {
+        let all = call.has_flag("all");
+        let long = call.has_flag("long");
+        let short_names = call.has_flag("short-names");
+
+        let call_span = call.head;
+
+        let (pattern, arg_span) = if let Some(expr) = call.positional.get(0) {
             let result = eval_expression(engine_state, stack, expr)?;
             let mut result = result.as_string()?;
 
@@ -49,12 +75,11 @@ impl Command for Ls {
                 result.push('*');
             }
 
-            result
+            (result, expr.span)
         } else {
-            "*".into()
+            ("*".into(), call_span)
         };
 
-        let call_span = call.head;
         let glob = glob::glob(&pattern).map_err(|err| {
             nu_protocol::ShellError::SpannedLabeledError(
                 "Error extracting glob pattern".into(),
@@ -63,67 +88,73 @@ impl Command for Ls {
             )
         })?;
 
+        let hidden_dir_specified = is_hidden_dir(&pattern);
+        let mut hidden_dirs = vec![];
+
         Ok(glob
             .into_iter()
-            .map(move |x| match x {
-                Ok(path) => match std::fs::symlink_metadata(&path) {
-                    Ok(metadata) => {
-                        let is_symlink = metadata.file_type().is_symlink();
-                        let is_file = metadata.is_file();
-                        let is_dir = metadata.is_dir();
-                        let filesize = metadata.len();
-                        let mut cols = vec!["name".into(), "type".into(), "size".into()];
-
-                        let mut vals = vec![
-                            Value::String {
-                                val: path.to_string_lossy().to_string(),
-                                span: call_span,
-                            },
-                            if is_symlink {
-                                Value::string("symlink", call_span)
-                            } else if is_file {
-                                Value::string("file", call_span)
-                            } else if is_dir {
-                                Value::string("dir", call_span)
-                            } else {
-                                Value::Nothing { span: call_span }
-                            },
-                            Value::Filesize {
-                                val: filesize as i64,
-                                span: call_span,
-                            },
-                        ];
-
-                        if let Ok(date) = metadata.modified() {
-                            let utc: DateTime<Utc> = date.into();
-
-                            cols.push("modified".into());
-                            vals.push(Value::Date {
-                                val: utc.into(),
-                                span: call_span,
-                            });
-                        }
-
-                        Value::Record {
-                            cols,
-                            vals,
-                            span: call_span,
-                        }
+            .filter_map(move |x| match x {
+                Ok(path) => {
+                    if permission_denied(&path) {
+                        #[cfg(unix)]
+                        let error_msg = format!(
+                            "The permissions of {:o} do not allow access for this user",
+                            path.metadata()
+                                .expect(
+                                    "this shouldn't be called since we already know there is a dir"
+                                )
+                                .permissions()
+                                .mode()
+                                & 0o0777
+                        );
+                        #[cfg(not(unix))]
+                        let error_msg = String::from("Permission denied");
+                        return Some(Value::Error {
+                            error: ShellError::SpannedLabeledError(
+                                "Permission denied".into(),
+                                error_msg,
+                                arg_span,
+                            ),
+                        });
                     }
-                    Err(_) => Value::Record {
-                        cols: vec!["name".into(), "type".into(), "size".into()],
-                        vals: vec![
-                            Value::String {
-                                val: path.to_string_lossy().to_string(),
-                                span: call_span,
-                            },
-                            Value::Nothing { span: call_span },
-                            Value::Nothing { span: call_span },
-                        ],
-                        span: call_span,
-                    },
-                },
-                _ => Value::Nothing { span: call_span },
+                    // if is_empty_dir(&p) {
+                    //     return Ok(ActionStream::empty());
+                    // }
+
+                    let metadata = match std::fs::symlink_metadata(&path) {
+                        Ok(metadata) => Some(metadata),
+                        Err(e) => {
+                            if e.kind() == ErrorKind::PermissionDenied
+                                || e.kind() == ErrorKind::Other
+                            {
+                                None
+                            } else {
+                                return Some(Value::Error {
+                                    error: ShellError::IOError(format!("{}", e)),
+                                });
+                            }
+                        }
+                    };
+                    if path_contains_hidden_folder(&path, &hidden_dirs) {
+                        return None;
+                    }
+
+                    if !all && !hidden_dir_specified && is_hidden_dir(&path) {
+                        if path.is_dir() {
+                            hidden_dirs.push(path);
+                        }
+                        return None;
+                    }
+
+                    let entry =
+                        dir_entry_dict(&path, metadata.as_ref(), call_span, long, short_names);
+
+                    match entry {
+                        Ok(value) => Some(value),
+                        Err(err) => Some(Value::Error { error: err }),
+                    }
+                }
+                _ => Some(Value::Nothing { span: call_span }),
             })
             .into_pipeline_data_with_metadata(
                 PipelineMetadata {
@@ -133,3 +164,270 @@ impl Command for Ls {
             ))
     }
 }
+
+fn permission_denied(dir: impl AsRef<Path>) -> bool {
+    match dir.as_ref().read_dir() {
+        Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
+        Ok(_) => false,
+    }
+}
+
+fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
+    #[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
+        }
+    }
+
+    #[cfg(not(windows))]
+    {
+        dir.as_ref()
+            .file_name()
+            .map(|name| name.to_string_lossy().starts_with('.'))
+            .unwrap_or(false)
+    }
+}
+
+fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool {
+    let path_str = path.to_str().expect("failed to read path");
+    if folders
+        .iter()
+        .any(|p| path_str.starts_with(&p.to_str().expect("failed to read hidden paths")))
+    {
+        return true;
+    }
+    false
+}
+
+#[cfg(unix)]
+use std::os::unix::fs::FileTypeExt;
+use std::path::{Path, PathBuf};
+
+pub fn get_file_type(md: &std::fs::Metadata) -> &str {
+    let ft = md.file_type();
+    let mut file_type = "Unknown";
+    if ft.is_dir() {
+        file_type = "Dir";
+    } else if ft.is_file() {
+        file_type = "File";
+    } else if ft.is_symlink() {
+        file_type = "Symlink";
+    } else {
+        #[cfg(unix)]
+        {
+            if ft.is_block_device() {
+                file_type = "Block device";
+            } else if ft.is_char_device() {
+                file_type = "Char device";
+            } else if ft.is_fifo() {
+                file_type = "Pipe";
+            } else if ft.is_socket() {
+                file_type = "Socket";
+            }
+        }
+    }
+    file_type
+}
+
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn dir_entry_dict(
+    filename: &std::path::Path,
+    metadata: Option<&std::fs::Metadata>,
+    span: Span,
+    long: bool,
+    short_name: bool,
+) -> Result<Value, ShellError> {
+    let mut cols = vec![];
+    let mut vals = vec![];
+
+    let name = if short_name {
+        filename.file_name().and_then(|s| s.to_str())
+    } else {
+        filename.to_str()
+    }
+    .ok_or_else(|| {
+        ShellError::SpannedLabeledError(
+            format!("Invalid file name: {:}", filename.to_string_lossy()),
+            "invalid file name".into(),
+            span,
+        )
+    })?;
+
+    cols.push("name".into());
+    vals.push(Value::String {
+        val: name.to_string(),
+        span,
+    });
+
+    if let Some(md) = metadata {
+        cols.push("type".into());
+        vals.push(Value::String {
+            val: get_file_type(md).to_string(),
+            span,
+        });
+    } else {
+        cols.push("type".into());
+        vals.push(Value::nothing(span));
+    }
+
+    if long {
+        cols.push("target".into());
+        if let Some(md) = metadata {
+            if md.file_type().is_symlink() {
+                if let Ok(path_to_link) = filename.read_link() {
+                    vals.push(Value::String {
+                        val: path_to_link.to_string_lossy().to_string(),
+                        span,
+                    });
+                } else {
+                    vals.push(Value::String {
+                        val: "Could not obtain target file's path".to_string(),
+                        span,
+                    });
+                }
+            } else {
+                vals.push(Value::nothing(span));
+            }
+        }
+    }
+
+    if long {
+        if let Some(md) = metadata {
+            cols.push("readonly".into());
+            vals.push(Value::Bool {
+                val: md.permissions().readonly(),
+                span,
+            });
+
+            #[cfg(unix)]
+            {
+                use std::os::unix::fs::MetadataExt;
+                let mode = md.permissions().mode();
+                cols.push("mode".into());
+                vals.push(Value::String {
+                    val: umask::Mode::from(mode).to_string(),
+                    span,
+                });
+
+                let nlinks = md.nlink();
+                cols.push("num_links".into());
+                vals.push(Value::Int {
+                    val: nlinks as i64,
+                    span,
+                });
+
+                let inode = md.ino();
+                cols.push("inode".into());
+                vals.push(Value::Int {
+                    val: inode as i64,
+                    span,
+                });
+
+                cols.push("uid".into());
+                if let Some(user) = users::get_user_by_uid(md.uid()) {
+                    vals.push(Value::String {
+                        val: user.name().to_string_lossy().into(),
+                        span,
+                    });
+                } else {
+                    vals.push(Value::nothing(span))
+                }
+
+                cols.push("group".into());
+                if let Some(group) = users::get_group_by_gid(md.gid()) {
+                    vals.push(Value::String {
+                        val: group.name().to_string_lossy().into(),
+                        span,
+                    });
+                } else {
+                    vals.push(Value::nothing(span))
+                }
+            }
+        }
+    }
+
+    cols.push("size".to_string());
+    if let Some(md) = metadata {
+        if md.is_dir() {
+            let dir_size: u64 = md.len();
+
+            vals.push(Value::Filesize {
+                val: dir_size as i64,
+                span,
+            });
+        } else if md.is_file() {
+            vals.push(Value::Filesize {
+                val: md.len() as i64,
+                span,
+            });
+        } else if md.file_type().is_symlink() {
+            if let Ok(symlink_md) = filename.symlink_metadata() {
+                vals.push(Value::Filesize {
+                    val: symlink_md.len() as i64,
+                    span,
+                });
+            } else {
+                vals.push(Value::nothing(span));
+            }
+        }
+    } else {
+        vals.push(Value::nothing(span));
+    }
+
+    if let Some(md) = metadata {
+        if long {
+            cols.push("created".to_string());
+            if let Ok(c) = md.created() {
+                let utc: DateTime<Utc> = c.into();
+                vals.push(Value::Date {
+                    val: utc.into(),
+                    span,
+                });
+            } else {
+                vals.push(Value::nothing(span));
+            }
+
+            cols.push("accessed".to_string());
+            if let Ok(a) = md.accessed() {
+                let utc: DateTime<Utc> = a.into();
+                vals.push(Value::Date {
+                    val: utc.into(),
+                    span,
+                });
+            } else {
+                vals.push(Value::nothing(span));
+            }
+        }
+
+        cols.push("modified".to_string());
+        if let Ok(m) = md.modified() {
+            let utc: DateTime<Utc> = m.into();
+            vals.push(Value::Date {
+                val: utc.into(),
+                span,
+            });
+        } else {
+            vals.push(Value::nothing(span));
+        }
+    } else {
+        if long {
+            cols.push("created".to_string());
+            vals.push(Value::nothing(span));
+
+            cols.push("accessed".to_string());
+            vals.push(Value::nothing(span));
+        }
+
+        cols.push("modified".to_string());
+        vals.push(Value::nothing(span));
+    }
+
+    Ok(Value::Record { cols, vals, span })
+}
diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs
index 55e640860..b76512c99 100644
--- a/crates/nu-command/src/viewers/table.rs
+++ b/crates/nu-command/src/viewers/table.rs
@@ -1,11 +1,11 @@
 use lscolors::{LsColors, Style};
 use nu_color_config::{get_color_config, style_primitive};
-use nu_engine::env_to_string;
+use nu_engine::{env_to_string, CallExt};
 use nu_protocol::ast::{Call, PathMember};
 use nu_protocol::engine::{Command, EngineState, Stack};
 use nu_protocol::{
     Category, Config, DataSource, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
-    PipelineMetadata, ShellError, Signature, Span, Value, ValueStream,
+    PipelineMetadata, ShellError, Signature, Span, SyntaxShape, Value, ValueStream,
 };
 use nu_table::{StyledString, TextStyle, Theme};
 use std::sync::atomic::{AtomicBool, Ordering};
@@ -30,7 +30,14 @@ impl Command for Table {
     }
 
     fn signature(&self) -> nu_protocol::Signature {
-        Signature::build("table").category(Category::Viewers)
+        Signature::build("table")
+            .named(
+                "start_number",
+                SyntaxShape::Int,
+                "row number to start viewing from",
+                Some('n'),
+            )
+            .category(Category::Viewers)
     }
 
     fn run(
@@ -43,6 +50,8 @@ impl Command for Table {
         let ctrlc = engine_state.ctrlc.clone();
         let config = stack.get_config().unwrap_or_default();
         let color_hm = get_color_config(&config);
+        let start_num: Option<i64> = call.get_flag(engine_state, stack, "start_number")?;
+        let row_offset = start_num.unwrap_or_default() as usize;
 
         let term_width = if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
             (w - 1) as usize
@@ -52,7 +61,7 @@ impl Command for Table {
 
         match input {
             PipelineData::Value(Value::List { vals, .. }, ..) => {
-                let table = convert_to_table(0, &vals, ctrlc, &config, call.head)?;
+                let table = convert_to_table(row_offset, &vals, ctrlc, &config, call.head)?;
 
                 if let Some(table) = table {
                     let result = nu_table::draw_table(&table, term_width, &color_hm, &config);
@@ -153,27 +162,13 @@ impl Command for Table {
                 let head = call.head;
 
                 Ok(PagingTableCreator {
-                    row_offset: 0,
+                    row_offset,
                     config,
                     ctrlc: ctrlc.clone(),
                     head,
                     stream,
                 }
                 .into_pipeline_data(ctrlc))
-
-                // let table = convert_to_table(stream, ctrlc, &config)?;
-
-                // if let Some(table) = table {
-                //     let result = nu_table::draw_table(&table, term_width, &color_hm, &config);
-
-                //     Ok(Value::String {
-                //         val: result,
-                //         span: call.head,
-                //     }
-                //     .into_pipeline_data())
-                // } else {
-                //     Ok(PipelineData::new(call.head))
-                // }
             }
             PipelineData::Value(Value::Record { cols, vals, .. }, ..) => {
                 let mut output = vec![];