diff --git a/Cargo.lock b/Cargo.lock index 18661d5dd3..3e3b69b972 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 a003a55962..353ea04e51 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 f85f347466..8c0e77d52f 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 = 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 d95908ded8..4a0f54f78c 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 955d9684ba..c8537fd2f6 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 { - 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 = 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) -> 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) -> 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 { + 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 = 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 = 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 = 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 55e6408602..b76512c991 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 = 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![];