mirror of
https://github.com/nushell/nushell
synced 2025-01-14 14:14:13 +00:00
Add --directory (-D) flag to ls, list the directory itself instead of its contents (#5970)
* Avoid extending the directory without globs in `nu_engine::glob_from` * avoid joining a `*` to the directory without globs * remove checks on directory permission and whether it is empty The previous implemention of `nu_engine::glob_from` will extend the given directory even if it containes no glob pattern. This commit overcomes lack of consistency with the function `nu_glob::glob`. * Add flag -D to ls, to list the directory itself instead of its contents * add --directory (-d) flag to ls * correct the difference between the given path and the cwd * set default path to `.` instead of `./*` when --directory (-d) flag is true * add comments * add an example * add tests * fmt
This commit is contained in:
parent
125e60d06a
commit
221f36ca65
4 changed files with 113 additions and 58 deletions
|
@ -14,15 +14,17 @@ fn flag_completions() {
|
||||||
// Test completions for the 'ls' flags
|
// Test completions for the 'ls' flags
|
||||||
let suggestions = completer.complete("ls -", 4);
|
let suggestions = completer.complete("ls -", 4);
|
||||||
|
|
||||||
assert_eq!(12, suggestions.len());
|
assert_eq!(14, suggestions.len());
|
||||||
|
|
||||||
let expected: Vec<String> = vec![
|
let expected: Vec<String> = vec![
|
||||||
"--all".into(),
|
"--all".into(),
|
||||||
|
"--directory".into(),
|
||||||
"--du".into(),
|
"--du".into(),
|
||||||
"--full-paths".into(),
|
"--full-paths".into(),
|
||||||
"--help".into(),
|
"--help".into(),
|
||||||
"--long".into(),
|
"--long".into(),
|
||||||
"--short-names".into(),
|
"--short-names".into(),
|
||||||
|
"-D".into(),
|
||||||
"-a".into(),
|
"-a".into(),
|
||||||
"-d".into(),
|
"-d".into(),
|
||||||
"-f".into(),
|
"-f".into(),
|
||||||
|
|
|
@ -56,6 +56,11 @@ impl Command for Ls {
|
||||||
"Display the apparent directory size in place of the directory metadata size",
|
"Display the apparent directory size in place of the directory metadata size",
|
||||||
Some('d'),
|
Some('d'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"directory",
|
||||||
|
"List the specified directory itself instead of its contents",
|
||||||
|
Some('D'),
|
||||||
|
)
|
||||||
.category(Category::FileSystem)
|
.category(Category::FileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +76,7 @@ impl Command for Ls {
|
||||||
let short_names = call.has_flag("short-names");
|
let short_names = call.has_flag("short-names");
|
||||||
let full_paths = call.has_flag("full-paths");
|
let full_paths = call.has_flag("full-paths");
|
||||||
let du = call.has_flag("du");
|
let du = call.has_flag("du");
|
||||||
|
let directory = call.has_flag("directory");
|
||||||
let ctrl_c = engine_state.ctrlc.clone();
|
let ctrl_c = engine_state.ctrlc.clone();
|
||||||
let call_span = call.head;
|
let call_span = call.head;
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
@ -83,7 +89,8 @@ impl Command for Ls {
|
||||||
let mut p = expand_to_real_path(p.item);
|
let mut p = expand_to_real_path(p.item);
|
||||||
|
|
||||||
let expanded = nu_path::expand_path_with(&p, &cwd);
|
let expanded = nu_path::expand_path_with(&p, &cwd);
|
||||||
if expanded.is_dir() {
|
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
||||||
|
if !directory && expanded.is_dir() {
|
||||||
if permission_denied(&p) {
|
if permission_denied(&p) {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let error_msg = format!(
|
let error_msg = format!(
|
||||||
|
@ -116,7 +123,10 @@ impl Command for Ls {
|
||||||
(p, p_tag, absolute_path)
|
(p, p_tag, absolute_path)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if is_empty_dir(current_dir(engine_state, stack)?) {
|
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
||||||
|
if directory {
|
||||||
|
(PathBuf::from("."), call_span, false)
|
||||||
|
} else if is_empty_dir(current_dir(engine_state, stack)?) {
|
||||||
return Ok(Value::nothing(call_span).into_pipeline_data());
|
return Ok(Value::nothing(call_span).into_pipeline_data());
|
||||||
} else {
|
} else {
|
||||||
(PathBuf::from("./*"), call_span, false)
|
(PathBuf::from("./*"), call_span, false)
|
||||||
|
@ -178,13 +188,30 @@ impl Command for Ls {
|
||||||
Some(path.to_string_lossy().to_string())
|
Some(path.to_string_lossy().to_string())
|
||||||
} else if let Some(prefix) = &prefix {
|
} else if let Some(prefix) = &prefix {
|
||||||
if let Ok(remainder) = path.strip_prefix(&prefix) {
|
if let Ok(remainder) = path.strip_prefix(&prefix) {
|
||||||
let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) {
|
if directory {
|
||||||
pfx
|
// When the path is the same as the cwd, path_diff should be "."
|
||||||
} else {
|
let path_diff =
|
||||||
prefix.to_path_buf()
|
if let Some(path_diff_not_dot) = diff_paths(&path, &cwd) {
|
||||||
};
|
let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
|
||||||
|
if path_diff_not_dot.is_empty() {
|
||||||
|
".".to_string()
|
||||||
|
} else {
|
||||||
|
path_diff_not_dot.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.to_string_lossy().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
Some(new_prefix.join(remainder).to_string_lossy().to_string())
|
Some(path_diff)
|
||||||
|
} else {
|
||||||
|
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 {
|
} else {
|
||||||
Some(path.to_string_lossy().to_string())
|
Some(path.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
@ -268,6 +295,12 @@ impl Command for Ls {
|
||||||
example: "ls -s ~ | where type == dir && modified < ((date now) - 7day)",
|
example: "ls -s ~ | where type == dir && modified < ((date now) - 7day)",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "List given paths, show directories themselves",
|
||||||
|
example:
|
||||||
|
"['/path/to/directory' '/path/to/file'] | each { |it| ls -D $it } | flatten",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -392,6 +392,74 @@ fn list_all_columns() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_with_directory_flag() {
|
||||||
|
Playground::setup("ls_test_flag_directory_1", |dirs, sandbox| {
|
||||||
|
sandbox
|
||||||
|
.within("dir_files")
|
||||||
|
.with_files(vec![EmptyFile("nushell.json")])
|
||||||
|
.within("dir_empty");
|
||||||
|
let actual = nu!(
|
||||||
|
cwd: dirs.test(), pipeline(
|
||||||
|
r#"
|
||||||
|
cd dir_empty;
|
||||||
|
['.' '././.' '..' '../dir_files' '../dir_files/*']
|
||||||
|
| each { |it| ls --directory $it }
|
||||||
|
| flatten
|
||||||
|
| get name
|
||||||
|
| to text
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
let expected = [".", ".", "..", "../dir_files", "../dir_files/nushell.json"].join("");
|
||||||
|
#[cfg(windows)]
|
||||||
|
let expected = expected.replace("/", "\\");
|
||||||
|
assert_eq!(
|
||||||
|
actual.out, expected,
|
||||||
|
"column names are incorrect for ls --directory (-D)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_with_directory_flag_without_argument() {
|
||||||
|
Playground::setup("ls_test_flag_directory_2", |dirs, sandbox| {
|
||||||
|
sandbox
|
||||||
|
.within("dir_files")
|
||||||
|
.with_files(vec![EmptyFile("nushell.json")])
|
||||||
|
.within("dir_empty");
|
||||||
|
// Test if there are some files in the current directory
|
||||||
|
let actual = nu!(
|
||||||
|
cwd: dirs.test(), pipeline(
|
||||||
|
r#"
|
||||||
|
cd dir_files;
|
||||||
|
ls --directory
|
||||||
|
| get name
|
||||||
|
| to text
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
let expected = ".";
|
||||||
|
assert_eq!(
|
||||||
|
actual.out, expected,
|
||||||
|
"column names are incorrect for ls --directory (-D)"
|
||||||
|
);
|
||||||
|
// Test if there is no file in the current directory
|
||||||
|
let actual = nu!(
|
||||||
|
cwd: dirs.test(), pipeline(
|
||||||
|
r#"
|
||||||
|
cd dir_empty;
|
||||||
|
ls -D
|
||||||
|
| get name
|
||||||
|
| to text
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
let expected = ".";
|
||||||
|
assert_eq!(
|
||||||
|
actual.out, expected,
|
||||||
|
"column names are incorrect for ls --directory (-D)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Rust's fs::metadata function is unable to read info for certain system files on Windows,
|
/// Rust's fs::metadata function is unable to read info for certain system files on Windows,
|
||||||
/// like the `C:\Windows\System32\Configuration` folder. https://github.com/rust-lang/rust/issues/96980
|
/// like the `C:\Windows\System32\Configuration` folder. https://github.com/rust-lang/rust/issues/96980
|
||||||
/// This test confirms that Nu can work around this successfully.
|
/// This test confirms that Nu can work around this successfully.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
use nu_glob::MatchOptions;
|
use nu_glob::MatchOptions;
|
||||||
|
@ -48,39 +46,7 @@ pub fn glob_from(
|
||||||
} else {
|
} else {
|
||||||
return Err(ShellError::DirectoryNotFound(pattern.span, None));
|
return Err(ShellError::DirectoryNotFound(pattern.span, None));
|
||||||
};
|
};
|
||||||
|
(path.parent().map(|parent| parent.to_path_buf()), path)
|
||||||
if path.is_dir() {
|
|
||||||
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 Err(ShellError::GenericError(
|
|
||||||
"Permission denied".into(),
|
|
||||||
error_msg,
|
|
||||||
Some(pattern.span),
|
|
||||||
None,
|
|
||||||
Vec::new(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_empty_dir(&path) {
|
|
||||||
return Ok((Some(path), Box::new(vec![].into_iter())));
|
|
||||||
}
|
|
||||||
|
|
||||||
(Some(path.clone()), path.join("*"))
|
|
||||||
} else {
|
|
||||||
(path.parent().map(|parent| parent.to_path_buf()), path)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let pattern = pattern.to_string_lossy().to_string();
|
let pattern = pattern.to_string_lossy().to_string();
|
||||||
|
@ -110,17 +76,3 @@ pub fn glob_from(
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue