mirror of
https://github.com/nushell/nushell
synced 2025-01-09 11:49:00 +00:00
b00098ccc6
* opt: improve ls by call get_file_type only one time * fmt * cs
580 lines
19 KiB
Rust
580 lines
19 KiB
Rust
use crate::DirBuilder;
|
|
use crate::DirInfo;
|
|
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
|
use nu_engine::env::current_dir;
|
|
use nu_engine::CallExt;
|
|
use nu_path::expand_to_real_path;
|
|
use nu_protocol::ast::Call;
|
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
|
use nu_protocol::{
|
|
Category, DataSource, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
|
|
PipelineMetadata, ShellError, Signature, Span, Spanned, SyntaxShape, Value,
|
|
};
|
|
use pathdiff::diff_paths;
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
#[derive(Clone)]
|
|
pub struct Ls;
|
|
|
|
impl Command for Ls {
|
|
fn name(&self) -> &str {
|
|
"ls"
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"List the files in a directory."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["dir"]
|
|
}
|
|
|
|
fn signature(&self) -> nu_protocol::Signature {
|
|
Signature::build("ls")
|
|
// Using a string instead of a glob pattern shape so it won't auto-expand
|
|
.optional("pattern", SyntaxShape::String, "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("full-paths", "display paths as absolute paths", Some('f'))
|
|
.switch(
|
|
"du",
|
|
"Display the apparent directory size in place of the directory metadata size",
|
|
Some('d'),
|
|
)
|
|
.category(Category::FileSystem)
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
|
let all = call.has_flag("all");
|
|
let long = call.has_flag("long");
|
|
let short_names = call.has_flag("short-names");
|
|
let full_paths = call.has_flag("full-paths");
|
|
let du = call.has_flag("du");
|
|
let ctrl_c = engine_state.ctrlc.clone();
|
|
let call_span = call.head;
|
|
let cwd = current_dir(engine_state, stack)?;
|
|
|
|
let pattern_arg: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
|
|
|
|
let (path, p_tag, absolute_path) = match pattern_arg {
|
|
Some(p) => {
|
|
let p_tag = p.span;
|
|
let mut p = expand_to_real_path(p.item);
|
|
|
|
let expanded = nu_path::expand_path_with(&p, &cwd);
|
|
if expanded.is_dir() {
|
|
if permission_denied(&p) {
|
|
#[cfg(unix)]
|
|
let error_msg = format!(
|
|
"The permissions of {:o} do not allow access for this user",
|
|
expanded
|
|
.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 Err(ShellError::GenericError(
|
|
"Permission denied".to_string(),
|
|
error_msg,
|
|
Some(p_tag),
|
|
None,
|
|
Vec::new(),
|
|
));
|
|
}
|
|
if is_empty_dir(&expanded) {
|
|
return Ok(Value::nothing(call_span).into_pipeline_data());
|
|
}
|
|
p.push("*");
|
|
}
|
|
let absolute_path = p.is_absolute();
|
|
(p, p_tag, absolute_path)
|
|
}
|
|
None => {
|
|
if is_empty_dir(current_dir(engine_state, stack)?) {
|
|
return Ok(Value::nothing(call_span).into_pipeline_data());
|
|
} else {
|
|
(PathBuf::from("./*"), call_span, false)
|
|
}
|
|
}
|
|
};
|
|
|
|
let hidden_dir_specified = is_hidden_dir(&path);
|
|
|
|
let glob_path = Spanned {
|
|
item: path.display().to_string(),
|
|
span: p_tag,
|
|
};
|
|
let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span)?;
|
|
|
|
let mut paths_peek = paths.peekable();
|
|
if paths_peek.peek().is_none() {
|
|
return Err(ShellError::GenericError(
|
|
format!("No matches found for {}", &path.display().to_string()),
|
|
"".to_string(),
|
|
None,
|
|
Some("no matches found".to_string()),
|
|
Vec::new(),
|
|
));
|
|
}
|
|
|
|
let mut hidden_dirs = vec![];
|
|
|
|
Ok(paths_peek
|
|
.into_iter()
|
|
.filter_map(move |x| match x {
|
|
Ok(path) => {
|
|
let metadata = match std::fs::symlink_metadata(&path) {
|
|
Ok(metadata) => Some(metadata),
|
|
Err(_) => None,
|
|
};
|
|
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 display_name = if short_names {
|
|
path.file_name().map(|os| os.to_string_lossy().to_string())
|
|
} else if full_paths || absolute_path {
|
|
Some(path.to_string_lossy().to_string())
|
|
} else if let Some(prefix) = &prefix {
|
|
if let Ok(remainder) = path.strip_prefix(&prefix) {
|
|
let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) {
|
|
pfx
|
|
} else {
|
|
prefix.to_path_buf()
|
|
};
|
|
|
|
Some(new_prefix.join(remainder).to_string_lossy().to_string())
|
|
} else {
|
|
Some(path.to_string_lossy().to_string())
|
|
}
|
|
} else {
|
|
Some(path.to_string_lossy().to_string())
|
|
}
|
|
.ok_or_else(|| {
|
|
ShellError::GenericError(
|
|
format!("Invalid file name: {:}", path.to_string_lossy()),
|
|
"invalid file name".into(),
|
|
Some(call_span),
|
|
None,
|
|
Vec::new(),
|
|
)
|
|
});
|
|
|
|
match display_name {
|
|
Ok(name) => {
|
|
let entry = dir_entry_dict(
|
|
&path,
|
|
&name,
|
|
metadata.as_ref(),
|
|
call_span,
|
|
long,
|
|
du,
|
|
ctrl_c.clone(),
|
|
);
|
|
match entry {
|
|
Ok(value) => Some(value),
|
|
Err(err) => Some(Value::Error { error: err }),
|
|
}
|
|
}
|
|
Err(err) => Some(Value::Error { error: err }),
|
|
}
|
|
}
|
|
_ => Some(Value::Nothing { span: call_span }),
|
|
})
|
|
.into_pipeline_data_with_metadata(
|
|
PipelineMetadata {
|
|
data_source: DataSource::Ls,
|
|
},
|
|
engine_state.ctrlc.clone(),
|
|
))
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "List all files in the current directory",
|
|
example: "ls",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "List all files in a subdirectory",
|
|
example: "ls subdir",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "List all files with full path in the parent directory",
|
|
example: "ls -f ..",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "List all rust files",
|
|
example: "ls *.rs",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "List all files and directories whose name do not contain 'bar'",
|
|
example: "ls -s | where name !~ bar",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "List all dirs in your home directory",
|
|
example: "ls ~ | where type == dir",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description:
|
|
"List all dirs in your home directory which have not been modified in 7 days",
|
|
example: "ls -s ~ | where type == dir && modified < ((date now) - 7day)",
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
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_empty_dir(dir: impl AsRef<Path>) -> bool {
|
|
match dir.as_ref().read_dir() {
|
|
Err(_) => true,
|
|
Ok(mut s) => s.next().is_none(),
|
|
}
|
|
}
|
|
|
|
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;
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
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, // absolute path
|
|
display_name: &str, // gile name to be displayed
|
|
metadata: Option<&std::fs::Metadata>,
|
|
span: Span,
|
|
long: bool,
|
|
du: bool,
|
|
ctrl_c: Option<Arc<AtomicBool>>,
|
|
) -> Result<Value, ShellError> {
|
|
let mut cols = vec![];
|
|
let mut vals = vec![];
|
|
let mut file_type = "unknown";
|
|
|
|
cols.push("name".into());
|
|
vals.push(Value::String {
|
|
val: display_name.to_string(),
|
|
span,
|
|
});
|
|
|
|
if let Some(md) = metadata {
|
|
file_type = get_file_type(md);
|
|
cols.push("type".into());
|
|
vals.push(Value::String {
|
|
val: file_type.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 {
|
|
let zero_sized =
|
|
file_type == "socket" || file_type == "block device" || file_type == "char device";
|
|
|
|
if md.is_dir() {
|
|
if du {
|
|
let params = DirBuilder::new(Span { start: 0, end: 2 }, None, false, None, false);
|
|
let dir_size = DirInfo::new(filename, ¶ms, None, ctrl_c).get_size();
|
|
|
|
vals.push(Value::Filesize {
|
|
val: dir_size as i64,
|
|
span,
|
|
});
|
|
} else {
|
|
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 {
|
|
let value = if zero_sized {
|
|
Value::Filesize { val: 0, span }
|
|
} else {
|
|
Value::nothing(span)
|
|
};
|
|
vals.push(value);
|
|
}
|
|
} else {
|
|
vals.push(Value::nothing(span));
|
|
}
|
|
|
|
if let Some(md) = metadata {
|
|
if long {
|
|
cols.push("created".to_string());
|
|
{
|
|
let mut val = Value::nothing(span);
|
|
if let Ok(c) = md.created() {
|
|
if let Some(local) = try_convert_to_local_date_time(c) {
|
|
val = Value::Date {
|
|
val: local.with_timezone(local.offset()),
|
|
span,
|
|
};
|
|
}
|
|
}
|
|
vals.push(val);
|
|
}
|
|
|
|
cols.push("accessed".to_string());
|
|
{
|
|
let mut val = Value::nothing(span);
|
|
if let Ok(a) = md.accessed() {
|
|
if let Some(local) = try_convert_to_local_date_time(a) {
|
|
val = Value::Date {
|
|
val: local.with_timezone(local.offset()),
|
|
span,
|
|
};
|
|
}
|
|
}
|
|
vals.push(val);
|
|
}
|
|
}
|
|
|
|
cols.push("modified".to_string());
|
|
{
|
|
let mut val = Value::nothing(span);
|
|
if let Ok(m) = md.modified() {
|
|
if let Some(local) = try_convert_to_local_date_time(m) {
|
|
val = Value::Date {
|
|
val: local.with_timezone(local.offset()),
|
|
span,
|
|
};
|
|
}
|
|
}
|
|
vals.push(val);
|
|
}
|
|
} 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 })
|
|
}
|
|
|
|
fn try_convert_to_local_date_time(t: SystemTime) -> Option<DateTime<Local>> {
|
|
// Adapted from https://github.com/chronotope/chrono/blob/v0.4.19/src/datetime.rs#L755-L767.
|
|
let (sec, nsec) = match t.duration_since(UNIX_EPOCH) {
|
|
Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
|
|
Err(e) => {
|
|
// unlikely but should be handled
|
|
let dur = e.duration();
|
|
let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos());
|
|
if nsec == 0 {
|
|
(-sec, 0)
|
|
} else {
|
|
(-sec - 1, 1_000_000_000 - nsec)
|
|
}
|
|
}
|
|
};
|
|
|
|
match Utc.timestamp_opt(sec, nsec) {
|
|
LocalResult::Single(t) => Some(t.with_timezone(&Local)),
|
|
_ => None,
|
|
}
|
|
}
|