diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index c1f86023b..e5064f6a1 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -45,6 +45,97 @@ fn lists_regular_files_using_asterisk_wildcard() { }) } +#[cfg(not(target_os = "windows"))] +#[test] +fn lists_regular_files_in_special_folder() { + Playground::setup("ls_test_3", |dirs, sandbox| { + sandbox + .mkdir("[abcd]") + .mkdir("[bbcd]") + .mkdir("abcd]") + .mkdir("abcd") + .mkdir("abcd/*") + .mkdir("abcd/?") + .with_files(vec![EmptyFile("[abcd]/test.txt")]) + .with_files(vec![EmptyFile("abcd]/test.txt")]) + .with_files(vec![EmptyFile("abcd/*/test.txt")]) + .with_files(vec![EmptyFile("abcd/?/test.txt")]) + .with_files(vec![EmptyFile("abcd/?/test2.txt")]); + + let actual = nu!( + cwd: dirs.test().join("abcd]"), format!(r#"ls | length"#)); + assert_eq!(actual.out, "1"); + let actual = nu!( + cwd: dirs.test(), format!(r#"ls abcd] | length"#)); + assert_eq!(actual.out, "1"); + let actual = nu!( + cwd: dirs.test().join("[abcd]"), format!(r#"ls | length"#)); + assert_eq!(actual.out, "1"); + let actual = nu!( + cwd: dirs.test().join("[bbcd]"), format!(r#"ls | length"#)); + assert_eq!(actual.out, "0"); + let actual = nu!( + cwd: dirs.test().join("abcd/*"), format!(r#"ls | length"#)); + assert_eq!(actual.out, "1"); + let actual = nu!( + cwd: dirs.test().join("abcd/?"), format!(r#"ls | length"#)); + assert_eq!(actual.out, "2"); + let actual = nu!( + cwd: dirs.test().join("abcd/*"), format!(r#"ls -D ../* | length"#)); + assert_eq!(actual.out, "2"); + let actual = nu!( + cwd: dirs.test().join("abcd/*"), format!(r#"ls ../* | length"#)); + assert_eq!(actual.out, "3"); + let actual = nu!( + cwd: dirs.test().join("abcd/?"), format!(r#"ls -D ../* | length"#)); + assert_eq!(actual.out, "2"); + let actual = nu!( + cwd: dirs.test().join("abcd/?"), format!(r#"ls ../* | length"#)); + assert_eq!(actual.out, "3"); + }) +} + +#[rstest::rstest] +#[case("j?.??.txt", 1)] +#[case("j????.txt", 2)] +#[case("?????.txt", 3)] +#[case("????c.txt", 1)] +#[case("ye??da.10.txt", 1)] +#[case("yehuda.?0.txt", 1)] +#[case("??????.10.txt", 2)] +#[case("[abcd]????.txt", 1)] +#[case("??[ac.]??.txt", 3)] +#[case("[ab]bcd/??.txt", 2)] +#[case("?bcd/[xy]y.txt", 2)] +#[case("?bcd/[xy]y.t?t", 2)] +#[case("[[]abcd[]].txt", 1)] +#[case("[[]?bcd[]].txt", 2)] +#[case("??bcd[]].txt", 2)] +#[case("??bcd].txt", 2)] +#[case("[[]?bcd].txt", 2)] +#[case("[[]abcd].txt", 1)] +#[case("[[][abcd]bcd[]].txt", 2)] +fn lists_regular_files_using_question_mark(#[case] command: &str, #[case] expected: usize) { + Playground::setup("ls_test_3", |dirs, sandbox| { + sandbox.mkdir("abcd").mkdir("bbcd").with_files(vec![ + EmptyFile("abcd/xy.txt"), + EmptyFile("bbcd/yy.txt"), + EmptyFile("[abcd].txt"), + EmptyFile("[bbcd].txt"), + EmptyFile("yehuda.10.txt"), + EmptyFile("jt.10.txt"), + EmptyFile("jtabc.txt"), + EmptyFile("abcde.txt"), + EmptyFile("andres.10.txt"), + EmptyFile("chicken_not_to_be_picked_up.100.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), format!(r#"ls {command} | length"#)); + assert_eq!(actual.out, expected.to_string()); + }) +} + #[test] fn lists_regular_files_using_question_mark_wildcard() { Playground::setup("ls_test_3", |dirs, sandbox| { diff --git a/crates/nu-engine/src/glob_from.rs b/crates/nu-engine/src/glob_from.rs index 2490ded5c..bd3aebf73 100644 --- a/crates/nu-engine/src/glob_from.rs +++ b/crates/nu-engine/src/glob_from.rs @@ -7,6 +7,8 @@ use nu_glob::MatchOptions; use nu_path::{canonicalize_with, expand_path_with}; use nu_protocol::{ShellError, Span, Spanned}; +const GLOB_CHARS: &[char] = &['*', '?', '[']; + /// This function is like `nu_glob::glob` from the `glob` crate, except it is relative to a given cwd. /// /// It returns a tuple of two values: the first is an optional prefix that the expanded filenames share. @@ -27,25 +29,16 @@ pub fn glob_from( ), ShellError, > { - let path = PathBuf::from(&pattern.item); - let path = expand_path_with(path, cwd); - let is_symlink = match fs::symlink_metadata(&path) { - Ok(attr) => attr.file_type().is_symlink(), - Err(_) => false, - }; - - // Check for brackets first - let (prefix, pattern) = if path.to_string_lossy().contains('[') { - // Path is a glob pattern => do not check for existence - // Select the longest prefix until the first '*' + let (prefix, pattern) = if pattern.item.contains(GLOB_CHARS) { + // Pattern contains glob, split it let mut p = PathBuf::new(); + let path = PathBuf::from(&pattern.item); let components = path.components(); let mut counter = 0; - // Get the path up to the pattern which we'll call the prefix for c in components { if let Component::Normal(os) = c { - if os.to_string_lossy().contains('*') { + if os.to_string_lossy().contains(GLOB_CHARS) { break; } } @@ -53,7 +46,6 @@ pub fn glob_from( counter += 1; } - // Let's separate the pattern from the path and we'll call this the pattern let mut just_pattern = PathBuf::new(); for c in counter..path.components().count() { if let Some(comp) = path.components().nth(c) { @@ -61,30 +53,29 @@ pub fn glob_from( } } - (Some(p), just_pattern) - } else if path.to_string_lossy().contains('*') { - // Path is a glob pattern => do not check for existence - // Select the longest prefix until the first '*' - let mut p = PathBuf::new(); - for c in path.components() { - if let Component::Normal(os) = c { - if os.to_string_lossy().contains('*') { - break; - } - } - p.push(c); - } + // Now expand `p` to get full prefix + let path = expand_path_with(p, cwd); + let escaped_prefix = PathBuf::from(nu_glob::Pattern::escape(&path.to_string_lossy())); - (Some(p), path) - } else if is_symlink { - (path.parent().map(|parent| parent.to_path_buf()), path) + (Some(path), escaped_prefix.join(just_pattern)) } else { - let path = if let Ok(p) = canonicalize_with(path, cwd) { - p - } else { - return Err(ShellError::DirectoryNotFound(pattern.span, None)); + let path = PathBuf::from(&pattern.item); + let path = expand_path_with(path, cwd); + let is_symlink = match fs::symlink_metadata(&path) { + Ok(attr) => attr.file_type().is_symlink(), + Err(_) => false, }; - (path.parent().map(|parent| parent.to_path_buf()), path) + + if is_symlink { + (path.parent().map(|parent| parent.to_path_buf()), path) + } else { + let path = if let Ok(p) = canonicalize_with(path, cwd) { + p + } else { + return Err(ShellError::DirectoryNotFound(pattern.span, None)); + }; + (path.parent().map(|parent| parent.to_path_buf()), path) + } }; let pattern = pattern.to_string_lossy().to_string(); diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 530ca6376..7729feebd 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -4629,7 +4629,9 @@ pub fn parse_value( SyntaxShape::Any | SyntaxShape::List(_) | SyntaxShape::Table(_) - | SyntaxShape::Signature => {} + | SyntaxShape::Signature + | SyntaxShape::Filepath + | SyntaxShape::String => {} _ => { working_set.error(ParseError::Expected("non-[] value", span)); return Expression::garbage(span);