├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gleam.toml ├── manifest.toml ├── src └── globlin.gleam └── test └── globlin_test.gleam /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: "26.0.2" 18 | gleam-version: "1.4.1" 19 | rebar3-version: "3" 20 | # elixir-version: "1.15.4" 21 | - run: gleam deps download 22 | - run: gleam test --target erlang 23 | - run: gleam test --target javascript 24 | - run: gleam format --check src test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v2.0.3 4 | - Support separate `gleam_regexp` package since the original regex module got removed from the `gleam_stdlib` package 5 | 6 | ## v2.0.2 7 | - Add note clarifying that this only supports Unix file paths right now 8 | 9 | ## v2.0.1 10 | - Add references to the `globlin_fs` file system package 11 | - Includes the file system logic removed in v2.0.0 12 | 13 | ## v2.0.0 - 2024-08-19 14 | - Remove file system dependencies so library can work in the browser 15 | - Remove `glob` and `glob_from` methods 16 | - Remove `simplifile` dependency 17 | - Panic on regex compile error 18 | - Remove `RegexCompileError` 19 | - It should never happen anyway as we convert the pattern to valid regex syntax 20 | 21 | ## v1.0.1 - 2024-08-18 22 | - Add in missing `simplifile` dependency 23 | - Add in missing glob methods 24 | 25 | ## v1.0.0 - 2024-08-18 26 | - First release! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kevin Robell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # globlin 2 | 3 | [![Package Version](https://img.shields.io/hexpm/v/globlin)](https://hex.pm/packages/globlin) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/globlin/) 5 | 6 | This package brings file globbing to Gleam. A `Pattern` is created by compiling the glob pattern string into the equivalent regex internally. This pattern can then be compared against other strings to find matching paths. 7 | 8 | Note: This library doesn't include methods to directly query the file system so that it can be used in the browser where that isn't available. If you're looking for file system globbing, check out the [globlin_fs](https://hexdocs.pm/globlin_fs/index.html) package. 9 | 10 | Note 2: This library only currently supports Unix file paths. That means it should work on Linux, macOS and BSD. 11 | 12 | ## Add Dependency 13 | 14 | ```sh 15 | gleam add globlin 16 | ``` 17 | 18 | ## Pattern Syntax 19 | 20 | There are seven special matching patterns supported by this package. They should be familiar to anyone who has used similar packages before. 21 | 22 | ### Question Mark `?` 23 | 24 | This matches any single character except the slash `/`. 25 | 26 | ### Star `*` 27 | 28 | This matches zero or more characters except the slash `/`. 29 | 30 | ### Globstar `**` 31 | 32 | This matches zero or more directories. It must be surrounded by the end of the string or slash `/`. 33 | 34 | Examples: 35 | - Isolated: `**` 36 | - Prefix: `**/tail` 37 | - Infix: `head/**/tail` 38 | - Postfix: `head/**` 39 | 40 | Note: When found at the end of the pattern, it matches all directories and files. 41 | 42 | ### Inclusive Char Set `[abc]` 43 | 44 | This matches any character in the set. 45 | 46 | ### Exclusive Char Set `[!abc]` 47 | 48 | This matches any character not in the set when the exclamation point `!` follows the opening square bracket. 49 | 50 | ### Inclusive Range `[a-z]` 51 | 52 | This matches any character from start to finish. 53 | 54 | ### Exclusive Range `[!a-z]` 55 | 56 | This matches any character not included in a range. 57 | 58 | ## Option Flags 59 | 60 | There are two option flags available to change the behavior of matching. They are both turned off by default when using the `new_pattern` method. 61 | 62 | ### `ignore_case` 63 | 64 | This changes all matches to be case insensitive. 65 | 66 | ### `match_dotfiles` 67 | 68 | Allow wildcards like `?`, `*` and `**` to match dotfiles. 69 | 70 | ## Example 71 | 72 | ```gleam 73 | import gleam/io 74 | import gleam/list 75 | import globlin 76 | 77 | pub fn main() { 78 | let files = [ 79 | ".gitignore", "gleam.toml", "LICENCE", "manifest.toml", "README.md", 80 | "src/globlin.gleam", "test/globlin_test.gleam", 81 | ] 82 | 83 | let assert Ok(pattern) = globlin.new_pattern("**/*.gleam") 84 | 85 | files 86 | |> list.filter(keeping: globlin.match_pattern(pattern:, path: _)) 87 | |> list.each(io.println) 88 | // src/globlin.gleam 89 | // test/globlin_test.gleam 90 | } 91 | ``` 92 | 93 | Further documentation can be found at . 94 | 95 | ## Development 96 | 97 | ```sh 98 | gleam test # Run the tests 99 | ``` 100 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "globlin" 2 | version = "2.0.3" 3 | description = "file globbing for Gleam" 4 | licences = ["MIT"] 5 | repository = { type = "github", user = "apainintheneck", repo = "globlin" } 6 | links = [{ title = "Globlin File System Package", href = "https://hexdocs.pm/globlin_fs/index.html" }] 7 | 8 | [dependencies] 9 | gleam_stdlib = ">= 0.34.0 and < 2.0.0" 10 | gleam_regexp = ">= 1.0.0 and < 2.0.0" 11 | 12 | [dev-dependencies] 13 | gleeunit = ">= 1.0.0 and < 2.0.0" 14 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, 6 | { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, 7 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 8 | ] 9 | 10 | [requirements] 11 | gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } 12 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 13 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 14 | -------------------------------------------------------------------------------- /src/globlin.gleam: -------------------------------------------------------------------------------- 1 | import gleam/list 2 | import gleam/regexp 3 | import gleam/string 4 | 5 | /// Each path pattern holds a compiled regex and options. 6 | pub opaque type Pattern { 7 | Pattern(regex: regexp.Regexp, options: PatternOptions) 8 | } 9 | 10 | /// Options that can be provided to the `new_pattern_with` method. 11 | /// 12 | /// - ignore_case: All matching is case insensitive (Default: False) 13 | /// - match_dotfiles: Match dotfiles when using wildcards (Default: False) 14 | pub type PatternOptions { 15 | PatternOptions(ignore_case: Bool, match_dotfiles: Bool) 16 | } 17 | 18 | const empty_options = PatternOptions(ignore_case: False, match_dotfiles: False) 19 | 20 | pub type PatternError { 21 | /// - The pattern must NOT start with a slash if compiled with a directory prefix. 22 | AbsolutePatternFromDirError 23 | /// - The globstar ("**") must always appear between the end of a string and/or a slash. 24 | InvalidGlobStarError 25 | /// - A char set or range was opened but never closed. 26 | MissingClosingBracketError 27 | } 28 | 29 | /// Compile a `Pattern` from a pattern. 30 | pub fn new_pattern(pattern: String) -> Result(Pattern, PatternError) { 31 | new_pattern_with(pattern, from: "", with: empty_options) 32 | } 33 | 34 | /// Compile a `Pattern` from a directory, pattern and options. 35 | /// The directory is escaped and prefixed before the pattern. 36 | pub fn new_pattern_with( 37 | pattern: String, 38 | from directory: String, 39 | with options: PatternOptions, 40 | ) -> Result(Pattern, PatternError) { 41 | case convert_pattern(directory, pattern, options) { 42 | Ok(pattern) -> { 43 | let regex_options = 44 | regexp.Options(case_insensitive: options.ignore_case, multi_line: False) 45 | case regexp.compile(pattern, with: regex_options) { 46 | Ok(regex) -> Ok(Pattern(regex:, options:)) 47 | // This should be unreachable as all converted patterns should be valid regex expressions. 48 | Error(err) -> { 49 | let error_message = 50 | "Globlin Regex Compile Bug: " 51 | <> "with directory '" 52 | <> directory 53 | <> "' and pattern '" 54 | <> pattern 55 | <> "': " 56 | <> err.error 57 | panic as error_message 58 | } 59 | } 60 | } 61 | Error(err) -> Error(err) 62 | } 63 | } 64 | 65 | /// Compare a `Pattern` against a path to see if they match. 66 | pub fn match_pattern(pattern pattern: Pattern, path path: String) -> Bool { 67 | regexp.check(with: pattern.regex, content: path) 68 | } 69 | 70 | // Convert path pattern graphemes into a regex syntax string. 71 | fn convert_pattern( 72 | prefix: String, 73 | pattern: String, 74 | options: PatternOptions, 75 | ) -> Result(String, PatternError) { 76 | let graphemes = string.to_graphemes(pattern) 77 | let path_chars = parse_path_chars(prefix) 78 | 79 | case graphemes, path_chars { 80 | ["/", ..], [_, ..] -> Error(AbsolutePatternFromDirError) 81 | _, _ -> { 82 | case do_convert_pattern(graphemes, path_chars, False, options) { 83 | Ok(regex_pattern) -> Ok("^" <> regex_pattern <> "$") 84 | Error(err) -> Error(err) 85 | } 86 | } 87 | } 88 | } 89 | 90 | // Escape all characters in the directory prefix and add a slash 91 | // before the following regex pattern if it's not already there. 92 | // 93 | // Note: The chars are returned in reverse order since we will 94 | // be prepending to them later on in the `do_convert_pattern` method. 95 | fn parse_path_chars(prefix: String) -> List(String) { 96 | prefix 97 | |> string.to_graphemes 98 | |> list.map(escape_meta_char) 99 | |> list.reverse 100 | |> fn(path_chars) { 101 | case path_chars { 102 | [] | ["/", ..] -> path_chars 103 | _ -> ["/", ..path_chars] 104 | } 105 | } 106 | } 107 | 108 | // Recursively convert path pattern graphemes into a regex syntax string. 109 | fn do_convert_pattern( 110 | graphemes: List(String), 111 | path_chars: List(String), 112 | in_range: Bool, 113 | options: PatternOptions, 114 | ) -> Result(String, PatternError) { 115 | case in_range { 116 | True -> { 117 | case graphemes { 118 | // Error since we've reached the end with an open char set 119 | [] -> Error(MissingClosingBracketError) 120 | // Unescaped closing bracket means the char set is finished 121 | ["]", ..rest] -> 122 | do_convert_pattern(rest, ["]", ..path_chars], False, options) 123 | // Continue on until we find the closing bracket 124 | ["\\", second, ..rest] -> 125 | [escape_meta_char(second), ..path_chars] 126 | |> do_convert_pattern(rest, _, True, options) 127 | [first, ..rest] -> 128 | [escape_meta_char(first), ..path_chars] 129 | |> do_convert_pattern(rest, _, True, options) 130 | } 131 | } 132 | False -> { 133 | case graphemes { 134 | // Success 135 | [] -> path_chars |> list.reverse |> string.concat |> Ok 136 | // Match empty brackets literally 137 | ["[", "]", ..rest] -> 138 | do_convert_pattern(rest, ["\\[\\]", ..path_chars], False, options) 139 | // Convert "[!" negative char set to regex format 140 | ["[", "!", ..rest] -> 141 | do_convert_pattern(rest, ["[^", ..path_chars], True, options) 142 | // Convert "[^" positive char set to regex format ("^" has no special meaning here) 143 | ["[", "^", ..rest] -> 144 | do_convert_pattern(rest, ["[\\^", ..path_chars], True, options) 145 | // Convert "[" positive char set to regex format 146 | ["[", ..rest] -> 147 | do_convert_pattern(rest, ["[", ..path_chars], True, options) 148 | // Escape any path chars preceded by a "\" only if necessary 149 | ["\\", second, ..rest] -> 150 | [escape_meta_char(second), ..path_chars] 151 | |> do_convert_pattern(rest, _, False, options) 152 | // Convert "?" which matches any char once to regex format 153 | ["?", ..rest] -> { 154 | let wildcard = case ignore_dotfiles(path_chars, options) { 155 | True -> "[^/.]" 156 | False -> "[^/]" 157 | } 158 | do_convert_pattern(rest, [wildcard, ..path_chars], False, options) 159 | } 160 | // Convert "**" to regex format 161 | ["*", "*", ..rest] -> { 162 | case path_chars, rest { 163 | // Isolated "**" matches zero or more directories or files 164 | // 165 | // Example: "**" 166 | [], [] -> { 167 | let wildcard = case options.match_dotfiles { 168 | True -> ".*" 169 | False -> "([^.][^/]*(/[^.][^/]*)*)?" 170 | } 171 | let path_chars = [wildcard, ..path_chars] 172 | do_convert_pattern(rest, path_chars, False, options) 173 | } 174 | // Postfix "**" matches zero or more directories or files 175 | // 176 | // Example: "filler/**" 177 | ["/", ..path_chars], [] -> { 178 | let wildcard = case options.match_dotfiles { 179 | True -> "(/.*)?" 180 | False -> "(/[^.][^/]*)*" 181 | } 182 | let path_chars = [wildcard, ..path_chars] 183 | do_convert_pattern(rest, path_chars, False, options) 184 | } 185 | // Prefix or infix "**" matches zero or more directories 186 | // 187 | // Examples: "**/filler" or "filler/**/filler" 188 | [], ["/", ..rest] | ["/", ..], ["/", ..rest] -> { 189 | let wildcard = case options.match_dotfiles { 190 | True -> "(.*/)?" 191 | False -> "([^.][^/]*/)*" 192 | } 193 | let path_chars = [wildcard, ..path_chars] 194 | do_convert_pattern(rest, path_chars, False, options) 195 | } 196 | _, _ -> Error(InvalidGlobStarError) 197 | } 198 | } 199 | // Convert "*" which matches any char zero or more times except "/" to regex format 200 | ["*", ..rest] -> { 201 | let wildcard = case ignore_dotfiles(path_chars, options) { 202 | True -> "([^.][^/]*)?" 203 | False -> "[^/]*" 204 | } 205 | do_convert_pattern(rest, [wildcard, ..path_chars], False, options) 206 | } 207 | // Escape any other path chars if necessary 208 | [first, ..rest] -> 209 | [escape_meta_char(first), ..path_chars] 210 | |> do_convert_pattern(rest, _, False, options) 211 | } 212 | } 213 | } 214 | } 215 | 216 | // Escape regex meta characters that should be matched literally inside the regex. 217 | fn escape_meta_char(char: String) -> String { 218 | case char { 219 | // Erlang: Metacharacters need to be escaped to avoid unexpected matching. 220 | // See https://www.erlang.org/doc/apps/stdlib/re.html#module-characters-and-metacharacters 221 | "\\" -> "\\\\" 222 | "^" -> "\\^" 223 | "$" -> "\\$" 224 | "." -> "\\." 225 | "[" -> "\\[" 226 | "|" -> "\\|" 227 | "(" -> "\\(" 228 | ")" -> "\\)" 229 | "?" -> "\\?" 230 | "*" -> "\\*" 231 | "+" -> "\\+" 232 | "{" -> "\\{" 233 | // JS: In unicode aware mode these need to be escaped explicitly. 234 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Regex_raw_bracket 235 | "]" -> "\\]" 236 | "}" -> "\\}" 237 | _ -> char 238 | } 239 | } 240 | 241 | // All wildcards ignore dotfiles by default unless the `match_dotfiles` 242 | // option is present. It is also possible to match dotfiles using literal dots, 243 | // char sets or ranges. 244 | fn ignore_dotfiles(path_chars: List(String), options: PatternOptions) -> Bool { 245 | !options.match_dotfiles && start_of_directory(path_chars) 246 | } 247 | 248 | // The start of a directory is the beginning of the path pattern or 249 | // anything immediately following a slash. 250 | fn start_of_directory(path_chars: List(String)) -> Bool { 251 | case path_chars { 252 | [] | [""] -> True 253 | [previous, ..] -> string.ends_with(previous, "/") 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /test/globlin_test.gleam: -------------------------------------------------------------------------------- 1 | /// Some of these tests are based on the tests in the Python standard library for the `fnmatch` library. 2 | /// 3 | /// Source: https://github.com/python/cpython/blob/e913d2c87f1ae4e7a4aef5ba78368ef31d060767/Lib/test/test_fnmatch.py 4 | /// 5 | import gleam/list 6 | import gleam/string 7 | import gleeunit 8 | import gleeunit/should 9 | import globlin 10 | 11 | pub fn main() { 12 | gleeunit.main() 13 | } 14 | 15 | type Pair { 16 | Pair(content: String, pattern: String) 17 | } 18 | 19 | const empty_options = globlin.PatternOptions( 20 | ignore_case: False, 21 | match_dotfiles: False, 22 | ) 23 | 24 | const no_case_options = globlin.PatternOptions( 25 | ignore_case: True, 26 | match_dotfiles: False, 27 | ) 28 | 29 | const with_dots_options = globlin.PatternOptions( 30 | ignore_case: False, 31 | match_dotfiles: True, 32 | ) 33 | 34 | fn check_pattern( 35 | pair pair: Pair, 36 | is_match is_match: Bool, 37 | options options: globlin.PatternOptions, 38 | ) -> Nil { 39 | globlin.new_pattern_with(pair.pattern, from: "", with: options) 40 | |> should.be_ok 41 | |> globlin.match_pattern(pair.content) 42 | |> should.equal(is_match) 43 | } 44 | 45 | pub fn simple_patterns_test() { 46 | [ 47 | Pair(content: "abc", pattern: "abc"), 48 | Pair(content: "abc", pattern: "?*?"), 49 | Pair(content: "abc", pattern: "???*"), 50 | Pair(content: "abc", pattern: "*???"), 51 | Pair(content: "abc", pattern: "???"), 52 | Pair(content: "abc", pattern: "*"), 53 | Pair(content: "abc", pattern: "ab[cd]"), 54 | Pair(content: "abc", pattern: "ab[!de]"), 55 | ] 56 | |> list.each(check_pattern(pair: _, is_match: True, options: empty_options)) 57 | 58 | [ 59 | Pair(content: "abc", pattern: "ab[de]"), 60 | Pair(content: "a", pattern: "??"), 61 | Pair(content: "a", pattern: "b"), 62 | ] 63 | |> list.each(check_pattern(pair: _, is_match: False, options: empty_options)) 64 | } 65 | 66 | pub fn paths_with_newlines_test() { 67 | [ 68 | Pair(content: "foo\nbar", pattern: "foo*"), 69 | Pair(content: "foo\nbar\n", pattern: "foo*"), 70 | Pair(content: "\nfoo", pattern: "\nfoo*"), 71 | Pair(content: "\n", pattern: "*"), 72 | ] 73 | |> list.each(check_pattern(pair: _, is_match: True, options: empty_options)) 74 | } 75 | 76 | pub fn slow_patterns_test() { 77 | [ 78 | Pair(content: string.repeat("a", 50), pattern: "*a*a*a*a*a*a*a*a*a*a"), 79 | Pair( 80 | content: string.repeat("a", 50) <> "b", 81 | pattern: "*a*a*a*a*a*a*a*a*a*ab", 82 | ), 83 | ] 84 | |> list.each(check_pattern(pair: _, is_match: True, options: empty_options)) 85 | } 86 | 87 | pub fn case_sensitivity_test() { 88 | [Pair(content: "abc", pattern: "abc"), Pair(content: "AbC", pattern: "AbC")] 89 | |> list.each(fn(pair) { 90 | check_pattern(pair: pair, is_match: True, options: empty_options) 91 | check_pattern(pair: pair, is_match: True, options: no_case_options) 92 | }) 93 | 94 | [Pair(content: "AbC", pattern: "abc"), Pair(content: "abc", pattern: "AbC")] 95 | |> list.each(fn(pair) { 96 | check_pattern(pair: pair, is_match: False, options: empty_options) 97 | check_pattern(pair: pair, is_match: True, options: no_case_options) 98 | }) 99 | } 100 | 101 | pub fn dotfiles_test() { 102 | [ 103 | Pair(content: ".secrets.txt", pattern: "*"), 104 | Pair(content: "repo/.git/config", pattern: "repo/**/config"), 105 | Pair(content: ".vimrc", pattern: "?vim*"), 106 | ] 107 | |> list.each(fn(pair) { 108 | check_pattern(pair: pair, is_match: False, options: empty_options) 109 | check_pattern(pair: pair, is_match: True, options: with_dots_options) 110 | }) 111 | 112 | [ 113 | Pair(content: "go/pkg/.mod/golang.org/", pattern: "go/*/.mod/*/"), 114 | Pair(content: ".vscode/argv.json", pattern: ".vscode/**"), 115 | Pair(content: "/path/README.md", pattern: "/path/README???"), 116 | ] 117 | |> list.each(fn(pair) { 118 | check_pattern(pair: pair, is_match: True, options: empty_options) 119 | check_pattern(pair: pair, is_match: True, options: with_dots_options) 120 | }) 121 | } 122 | 123 | pub fn globstar_test() { 124 | [ 125 | "**", "**/ghi", "**/def/**", "**/def/ghi", "abc/**", "abc/def/**", 126 | "**/abc/def/ghi", "abc/def/ghi/**", 127 | ] 128 | |> list.each(fn(pattern) { 129 | let pair = Pair(content: "abc/def/ghi", pattern:) 130 | check_pattern(pair:, is_match: True, options: empty_options) 131 | }) 132 | 133 | [ 134 | "hello_world.gleam", "hello.world.gleam", "hello/world.gleam", 135 | "he.llo/wo.rld.gleam", 136 | ] 137 | |> list.each(fn(content) { 138 | let pair = Pair(content:, pattern: "**/*.gleam") 139 | check_pattern(pair:, is_match: True, options: empty_options) 140 | }) 141 | } 142 | 143 | pub fn from_directory_test() { 144 | ["/home/", "/home"] 145 | |> list.each(fn(directory) { 146 | globlin.new_pattern_with( 147 | "documents/**/img_*.png", 148 | from: directory, 149 | with: empty_options, 150 | ) 151 | |> should.be_ok 152 | |> globlin.match_pattern( 153 | path: "/home/documents/mallorca_2012/img_beach.png", 154 | ) 155 | |> should.be_true 156 | }) 157 | } 158 | 159 | pub fn invalid_pattern_test() { 160 | ["[", "abc[def", "abc[def\\]g", "]]]][[]["] 161 | |> list.each(fn(pattern) { 162 | globlin.new_pattern(pattern) 163 | |> should.equal(Error(globlin.MissingClosingBracketError)) 164 | }) 165 | 166 | ["ab**cd", "one/two**/three", "four/**five/six", "**seven", "eight**"] 167 | |> list.each(fn(pattern) { 168 | globlin.new_pattern(pattern) 169 | |> should.equal(Error(globlin.InvalidGlobStarError)) 170 | }) 171 | 172 | globlin.new_pattern_with("/**/*.json", from: "/home", with: empty_options) 173 | |> should.equal(Error(globlin.AbsolutePatternFromDirError)) 174 | } 175 | 176 | // JS: In unicode aware mode these need to be escaped explicitly. 177 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Regex_raw_bracket 178 | pub fn raw_brackets_test() { 179 | [ 180 | "]", "[[[]]]", "[]]]]", "{", "}", "{{{}}}", "{}}}", "(", ")", "((()))", 181 | "()))", 182 | ] 183 | |> list.each(fn(pattern) { 184 | globlin.new_pattern(pattern) 185 | |> should.be_ok 186 | }) 187 | } 188 | 189 | pub fn readme_test() { 190 | let files = [ 191 | ".gitignore", "gleam.toml", "LICENCE", "manifest.toml", "README.md", 192 | "src/globlin.gleam", "test/globlin_test.gleam", 193 | ] 194 | 195 | let assert Ok(pattern) = globlin.new_pattern("**/*.gleam") 196 | 197 | files 198 | |> list.filter(keeping: globlin.match_pattern(pattern:, path: _)) 199 | |> should.equal(["src/globlin.gleam", "test/globlin_test.gleam"]) 200 | } 201 | --------------------------------------------------------------------------------