1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// utilities for expanding globs in command arguments

use nu_glob::{glob_with_parent, MatchOptions, Paths};
use nu_protocol::{ShellError, Spanned};
use std::fs;
use std::path::{Path, PathBuf};

// standard glob options to use for filesystem command arguments

const GLOB_PARAMS: MatchOptions = MatchOptions {
    case_sensitive: true,
    require_literal_separator: false,
    require_literal_leading_dot: false,
    recursive_match_hidden_dir: true,
};

// handle an argument that could be a literal path or a glob.
// if literal path, return just that (whether user can access it or not).
// if glob, expand into matching paths, using GLOB_PARAMS options.
pub fn arg_glob(
    pattern: &Spanned<String>, // alleged path or glob
    cwd: &Path,                // current working directory
) -> Result<Paths, ShellError> {
    arg_glob_opt(pattern, cwd, GLOB_PARAMS)
}

// variant of [arg_glob] that requires literal dot prefix in pattern to match dot-prefixed path.
pub fn arg_glob_leading_dot(pattern: &Spanned<String>, cwd: &Path) -> Result<Paths, ShellError> {
    arg_glob_opt(
        pattern,
        cwd,
        MatchOptions {
            require_literal_leading_dot: true,
            ..GLOB_PARAMS
        },
    )
}

fn arg_glob_opt(
    pattern: &Spanned<String>,
    cwd: &Path,
    options: MatchOptions,
) -> Result<Paths, ShellError> {
    // remove ansi coloring (?)
    let pattern = {
        Spanned {
            item: nu_utils::strip_ansi_string_unlikely(pattern.item.clone()),
            span: pattern.span,
        }
    };

    // if there's a file with same path as the pattern, just return that.
    let pp = cwd.join(&pattern.item);
    let md = fs::metadata(pp);
    #[allow(clippy::single_match)]
    match md {
        Ok(_metadata) => {
            return Ok(Paths::single(&PathBuf::from(pattern.item), cwd));
        }
        // file not found, but also "invalid chars in file" (e.g * on Windows).  Fall through and glob
        Err(_) => {}
    }

    // user wasn't referring to a specific thing in filesystem, try to glob it.
    match glob_with_parent(&pattern.item, options, cwd) {
        Ok(p) => Ok(p),
        Err(pat_err) => Err(ShellError::InvalidGlobPattern {
            msg: pat_err.msg.into(),
            span: pattern.span,
        }),
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use nu_glob::GlobResult;
    use nu_protocol::{Span, Spanned};
    use nu_test_support::fs::Stub::EmptyFile;
    use nu_test_support::playground::Playground;
    use rstest::rstest;

    fn spanned_string(str: &str) -> Spanned<String> {
        Spanned {
            item: str.to_string(),
            span: Span::test_data(),
        }
    }

    #[test]
    fn does_something() {
        let act = arg_glob(&spanned_string("*"), &PathBuf::from("."));
        assert!(act.is_ok());
        for f in act.expect("checked ok") {
            match f {
                Ok(p) => {
                    assert!(!p.to_str().unwrap().is_empty());
                }
                Err(e) => panic!("unexpected error {:?}", e),
            };
        }
    }

    #[test]
    fn glob_format_error() {
        let act = arg_glob(&spanned_string(r#"ab]c[def"#), &PathBuf::from("."));
        assert!(act.is_err());
    }

    #[rstest]
    #[case("*", 4, "no dirs")]
    #[case("**/*", 7, "incl dirs")]
    fn glob_subdirs(#[case] pat: &str, #[case] exp_count: usize, #[case] case: &str) {
        Playground::setup("glob_subdirs", |dirs, sandbox| {
            sandbox.with_files(vec![
                EmptyFile("yehuda.txt"),
                EmptyFile("jttxt"),
                EmptyFile("andres.txt"),
            ]);
            sandbox.mkdir(".children");
            sandbox.within(".children").with_files(vec![
                EmptyFile("timothy.txt"),
                EmptyFile("tiffany.txt"),
                EmptyFile("trish.txt"),
            ]);

            let p: Vec<GlobResult> = arg_glob(&spanned_string(pat), &dirs.test)
                .expect("no error")
                .collect();
            assert_eq!(
                exp_count,
                p.iter().filter(|i| i.is_ok()).count(),
                " case: {case} ",
            );

            // expected behavior -- that directories are included in results (if name matches pattern)
            let t = p
                .iter()
                .any(|i| i.as_ref().unwrap().to_string_lossy().contains(".children"));
            assert!(t, "check for dir, case {case}");
        })
    }

    #[rstest]
    #[case("yehuda.txt", true, 1, "matches literal path")]
    #[case("*", false, 3, "matches glob")]
    #[case(r#"bad[glob.foo"#, true, 1, "matches literal, would be bad glob pat")]
    fn exact_vs_glob(
        #[case] pat: &str,
        #[case] exp_matches_input: bool,
        #[case] exp_count: usize,
        #[case] case: &str,
    ) {
        Playground::setup("exact_vs_glob", |dirs, sandbox| {
            sandbox.with_files(vec![
                EmptyFile("yehuda.txt"),
                EmptyFile("jttxt"),
                EmptyFile("bad[glob.foo"),
            ]);

            let res = arg_glob(&spanned_string(pat), &dirs.test)
                .expect("no error")
                .collect::<Vec<GlobResult>>();

            eprintln!("res: {:?}", res);
            if exp_matches_input {
                assert_eq!(
                    exp_count,
                    res.len(),
                    " case {case}: matches input, but count not 1? "
                );
                assert_eq!(
                    &res[0].as_ref().unwrap().to_string_lossy(),
                    pat, // todo: is it OK for glob to return relative paths (not to current cwd, but to arg cwd of arg_glob)?
                );
            } else {
                assert_eq!(exp_count, res.len(), " case: {}: matched glob", case);
            }
        })
    }

    #[rstest]
    #[case(r#"realbad[glob.foo"#, true, 1, "error, bad glob")]
    fn exact_vs_bad_glob(
        // if path doesn't exist but pattern is not valid glob, should get error.
        #[case] pat: &str,
        #[case] _exp_matches_input: bool,
        #[case] _exp_count: usize,
        #[case] _tag: &str,
    ) {
        Playground::setup("exact_vs_bad_glob", |dirs, sandbox| {
            sandbox.with_files(vec![
                EmptyFile("yehuda.txt"),
                EmptyFile("jttxt"),
                EmptyFile("bad[glob.foo"),
            ]);

            let res = arg_glob(&spanned_string(pat), &dirs.test);
            assert!(res
                .expect_err("expected error")
                .to_string()
                .contains("Invalid glob pattern"));
        })
    }
}