├── bin ├── lua.exe ├── lua54.dll ├── concrt140.dll ├── lpeglabel.dll ├── msvcp140.dll ├── ucrtbase.dll ├── vcruntime140.dll ├── api-ms-win-core-file-l1-1-0.dll ├── api-ms-win-core-file-l1-2-0.dll ├── api-ms-win-core-file-l2-1-0.dll ├── api-ms-win-core-heap-l1-1-0.dll ├── api-ms-win-core-util-l1-1-0.dll ├── api-ms-win-crt-conio-l1-1-0.dll ├── api-ms-win-crt-heap-l1-1-0.dll ├── api-ms-win-crt-math-l1-1-0.dll ├── api-ms-win-crt-stdio-l1-1-0.dll ├── api-ms-win-crt-time-l1-1-0.dll ├── api-ms-win-core-console-l1-1-0.dll ├── api-ms-win-core-console-l1-2-0.dll ├── api-ms-win-core-debug-l1-1-0.dll ├── api-ms-win-core-handle-l1-1-0.dll ├── api-ms-win-core-memory-l1-1-0.dll ├── api-ms-win-core-profile-l1-1-0.dll ├── api-ms-win-core-string-l1-1-0.dll ├── api-ms-win-core-synch-l1-1-0.dll ├── api-ms-win-core-synch-l1-2-0.dll ├── api-ms-win-core-sysinfo-l1-1-0.dll ├── api-ms-win-crt-convert-l1-1-0.dll ├── api-ms-win-crt-locale-l1-1-0.dll ├── api-ms-win-crt-private-l1-1-0.dll ├── api-ms-win-crt-process-l1-1-0.dll ├── api-ms-win-crt-runtime-l1-1-0.dll ├── api-ms-win-crt-string-l1-1-0.dll ├── api-ms-win-crt-utility-l1-1-0.dll ├── api-ms-win-core-datetime-l1-1-0.dll ├── api-ms-win-core-namedpipe-l1-1-0.dll ├── api-ms-win-core-timezone-l1-1-0.dll ├── api-ms-win-crt-filesystem-l1-1-0.dll ├── api-ms-win-crt-multibyte-l1-1-0.dll ├── api-ms-win-core-interlocked-l1-1-0.dll ├── api-ms-win-core-localization-l1-2-0.dll ├── api-ms-win-core-rtlsupport-l1-1-0.dll ├── api-ms-win-crt-environment-l1-1-0.dll ├── api-ms-win-core-errorhandling-l1-1-0.dll ├── api-ms-win-core-libraryloader-l1-1-0.dll ├── api-ms-win-core-processthreads-l1-1-0.dll ├── api-ms-win-core-processthreads-l1-1-1.dll └── api-ms-win-core-processenvironment-l1-1-0.dll ├── .luarc.json ├── test ├── gitignore │ ├── init.lua │ ├── bug.lua │ ├── match.lua │ └── scan.lua ├── init.lua ├── glob.lua └── utility.lua ├── .vscode └── launch.json ├── lua-glob-scm-1.rockspec ├── LICENSE ├── README.md └── glob.lua /bin/lua.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/lua.exe -------------------------------------------------------------------------------- /bin/lua54.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/lua54.dll -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "diagnostics.disable": [ 3 | "deprecated" 4 | ] 5 | } -------------------------------------------------------------------------------- /bin/concrt140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/concrt140.dll -------------------------------------------------------------------------------- /bin/lpeglabel.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/lpeglabel.dll -------------------------------------------------------------------------------- /bin/msvcp140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/msvcp140.dll -------------------------------------------------------------------------------- /bin/ucrtbase.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/ucrtbase.dll -------------------------------------------------------------------------------- /bin/vcruntime140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/vcruntime140.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-file-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-file-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-file-l1-2-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-file-l1-2-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-file-l2-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-file-l2-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-heap-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-heap-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-util-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-util-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-conio-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-conio-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-heap-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-heap-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-math-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-math-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-stdio-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-stdio-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-time-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-time-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-console-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-console-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-console-l1-2-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-console-l1-2-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-debug-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-debug-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-handle-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-handle-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-memory-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-memory-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-profile-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-profile-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-string-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-string-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-synch-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-synch-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-synch-l1-2-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-synch-l1-2-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-sysinfo-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-sysinfo-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-convert-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-convert-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-locale-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-locale-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-private-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-private-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-process-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-process-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-runtime-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-runtime-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-string-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-string-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-utility-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-utility-l1-1-0.dll -------------------------------------------------------------------------------- /test/gitignore/init.lua: -------------------------------------------------------------------------------- 1 | require 'test.gitignore.match' 2 | require 'test.gitignore.scan' 3 | require 'test.gitignore.bug' 4 | -------------------------------------------------------------------------------- /bin/api-ms-win-core-datetime-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-datetime-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-namedpipe-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-namedpipe-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-timezone-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-timezone-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-filesystem-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-filesystem-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-multibyte-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-multibyte-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-interlocked-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-interlocked-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-localization-l1-2-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-localization-l1-2-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-rtlsupport-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-rtlsupport-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-crt-environment-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-crt-environment-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-errorhandling-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-errorhandling-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-libraryloader-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-libraryloader-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-processthreads-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-processthreads-l1-1-0.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-processthreads-l1-1-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-processthreads-l1-1-1.dll -------------------------------------------------------------------------------- /bin/api-ms-win-core-processenvironment-l1-1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumneko/lua-glob/HEAD/bin/api-ms-win-core-processenvironment-l1-1-0.dll -------------------------------------------------------------------------------- /test/init.lua: -------------------------------------------------------------------------------- 1 | package.path = '?/init.lua;' .. package.path 2 | 3 | print 'Test start.' 4 | 5 | require 'test.utility' 6 | require 'test.glob' 7 | require 'test.gitignore' 8 | 9 | print 'Test done.' 10 | -------------------------------------------------------------------------------- /test/gitignore/bug.lua: -------------------------------------------------------------------------------- 1 | local glob = require 'glob' 2 | 3 | do 4 | local session = glob.gitignore() 5 | session:setInterface('type', function (path) 6 | return 'directory' 7 | end) 8 | session:setInterface('list', function (path) 9 | return { 10 | '.', 11 | '/.', 12 | './', 13 | '/./', 14 | } 15 | end) 16 | session:scan() 17 | end 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lua", 9 | "request": "launch", 10 | "name": "Launch", 11 | "program": "${workspaceFolder}/test/init.lua", 12 | "path": "${workspaceFolder}/?.lua;${workspaceFolder}/?/init.lua", 13 | "cpath": "${workspaceFolder}/bin/?.dll", 14 | "stopOnEntry": false 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /lua-glob-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | rockspec_format = "3.0" 2 | package = "lua-glob" 3 | version = "scm-1" 4 | source = { 5 | url = "git+https://github.com/sumneko/lua-glob", 6 | branch = "master", 7 | } 8 | description = { 9 | summary = "glob and gitignore pattern matchers for lua", 10 | homepage = "https://github.com/sumneko/lua-glob", 11 | license = "MIT" 12 | } 13 | dependencies = { 14 | "lua >= 5.1", 15 | } 16 | build = { 17 | type = "builtin", 18 | modules = { 19 | ["glob"] = "glob.lua", 20 | }, 21 | } 22 | test = { 23 | type = "command", 24 | script = "test/init.lua", 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 最萌小汐 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 | # lua-glob 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/2u02fyusb1aw5rs9?svg=true)](https://ci.appveyor.com/project/sumneko/lua-glob) 4 | 5 | Install: copy "glob/" directory to your project, or use luarocks `luarocks install lua-glob --server=https://luarocks.org/dev` 6 | 7 | ## glob 8 | ```lua 9 | local glob = require 'glob' 10 | 11 | local pattern = { 12 | 'src', 13 | '!*.dll', 14 | } 15 | local options = { 16 | ignoreCase = true 17 | } 18 | local parser = glob.glob(pattern, options) 19 | 20 | parser 'Src/main.lua' --> true 21 | parser 'Src/lpeg.dll' --> false 22 | ``` 23 | 24 | ## gitignore 25 | 26 | ### match 27 | ```lua 28 | local glob = require 'glob' 29 | 30 | local pattern = { 31 | 'src/*', 32 | '!*.dll', 33 | } 34 | local options = { 35 | ignoreCase = true 36 | } 37 | local parser = glob.gitignore(pattern, options) 38 | 39 | parser 'Src/main.lua' --> true 40 | parser 'Src/lpeg.dll' --> false 41 | ``` 42 | 43 | ### scan 44 | Work space 45 | ``` 46 | main.lua 47 | utility.lua 48 | src 49 | |---test.lua 50 | |---bee.dll 51 | |---lua.dll 52 | ``` 53 | 54 | ```lua 55 | local glob = require 'glob' 56 | local fs = require 'bee.filesystem' -- just another filesystem 57 | 58 | 59 | local pattern = { 60 | 'src/*', 61 | '!*.dll', 62 | } 63 | local options = { 64 | ignoreCase = true 65 | } 66 | local interface = { 67 | type = function (path) 68 | if not fs.exists(fs.path(path)) then 69 | return nil 70 | end 71 | if fs.is_directory(fs.path(path)) then 72 | return 'directory' 73 | else 74 | return 'file' 75 | end 76 | end, 77 | list = function (path) 78 | if not fs.exists(fs.path(path)) then 79 | return nil 80 | end 81 | if not fs.is_directory(fs.path(path)) then 82 | return nil 83 | end 84 | local childs = {} 85 | for child in fs.path(path):list_directory() do 86 | childs[#childs+1] = child:string() 87 | end 88 | return childs 89 | end, 90 | } 91 | 92 | local parser = glob.gitignore(pattern, options, interface) 93 | local files = parser:scan() 94 | print(files[1]) --> main.lua 95 | print(files[2]) --> utility.lua 96 | print(files[3]) --> src\bee.dll 97 | print(files[4]) --> src\lua.dll 98 | print(files[5]) --> nil 99 | ``` 100 | -------------------------------------------------------------------------------- /test/gitignore/match.lua: -------------------------------------------------------------------------------- 1 | local glob = require 'glob' 2 | 3 | local mt = {} 4 | mt.__index = mt 5 | 6 | function mt:ok(path) 7 | assert(self.matcher(path) == true) 8 | return self 9 | end 10 | 11 | function mt:no(path) 12 | assert(self.matcher(path) ~= true) 13 | return self 14 | end 15 | 16 | function mt:op(key) 17 | self.matcher:setOption(key) 18 | return self 19 | end 20 | 21 | function mt:ft(func) 22 | self.matcher:setInterface('type', func) 23 | return self 24 | end 25 | 26 | local function test(pattern, options) 27 | local matcher = glob.gitignore(pattern, options) 28 | return setmetatable({ 29 | matcher = matcher 30 | }, mt) 31 | end 32 | 33 | print 'Test gitignore ...' 34 | test 'src/' 35 | : ok 'src/a' 36 | : ok 'a/src' 37 | 38 | test 'example.[0-9az]' 39 | : ok 'example.0' 40 | : ok 'example.5' 41 | : ok 'example.a' 42 | : ok 'example.z' 43 | : no 'example.' 44 | : no 'example.10' 45 | 46 | test 'example.[a0-9z]' 47 | : ok 'example.0' 48 | : ok 'example.5' 49 | : ok 'example.a' 50 | : ok 'example.z' 51 | : no 'example.' 52 | : no 'example.10' 53 | 54 | test 'example.\\[a0-9z\\]' 55 | : no 'example.0' 56 | : no 'example.5' 57 | : no 'example.a' 58 | : no 'example.z' 59 | : no 'example.' 60 | : no 'example.10' 61 | : ok 'example.[a0-9z]' 62 | 63 | test 'src/' 64 | : ft(function (path) 65 | if path == 'a/src' then 66 | return 'file' 67 | else 68 | return 'directory' 69 | end 70 | end) 71 | : ok 'src/a' 72 | : no 'a/src' 73 | 74 | test 'src/' 75 | : ft(function (path) 76 | return 'directory' 77 | end) 78 | : ok 'src/a' 79 | : ok 'a/src' 80 | 81 | test 'src/' 82 | : ft(function (path) 83 | return 'file' 84 | end) 85 | : ok 'src/a' 86 | : no 'a/src' 87 | 88 | test {'aaa', '!aaa/bbb'} 89 | : ft(function (path) 90 | if path == 'aaa' then 91 | return 'directory' 92 | else 93 | return 'file' 94 | end 95 | end) 96 | : ok 'aaa' 97 | : ok 'aaa/bbb' 98 | : ok 'aaa/ccc' 99 | 100 | test {'aaa/', '!aaa/bbb'} 101 | : ft(function (path) 102 | if path == 'aaa' then 103 | return 'directory' 104 | else 105 | return 'file' 106 | end 107 | end) 108 | : ok 'aaa' 109 | : ok 'aaa/bbb' 110 | : ok 'aaa/ccc' 111 | 112 | test {'aaa/*', '!aaa/bbb'} 113 | : ft(function (path) 114 | if path == 'aaa' then 115 | return 'directory' 116 | else 117 | return 'file' 118 | end 119 | end) 120 | : no 'aaa' 121 | : no 'aaa/bbb' 122 | : ok 'aaa/ccc' 123 | 124 | test {'/*', '!/usr'} 125 | : ft(function (path) 126 | if path == 'usr' then 127 | return 'directory' 128 | else 129 | return 'file' 130 | end 131 | end) 132 | : ok 'a.lua' 133 | : no 'usr/a.lua' 134 | 135 | test '/' 136 | : no '1' 137 | : no '2/1' 138 | 139 | test 'abc' 140 | : ok 'f/abc/a.lua' 141 | 142 | test 'abc' 143 | : ok '[f]/abc/a.lua' 144 | -------------------------------------------------------------------------------- /test/glob.lua: -------------------------------------------------------------------------------- 1 | local glob = require 'glob' 2 | 3 | local mt = {} 4 | mt.__index = mt 5 | 6 | function mt:ok(path) 7 | assert(self.matcher(path) == true) 8 | return self 9 | end 10 | 11 | function mt:no(path) 12 | assert(self.matcher(path) ~= true) 13 | return self 14 | end 15 | 16 | function mt:op(key) 17 | self.matcher:setOption(key) 18 | return self 19 | end 20 | 21 | local function test(pattern, options) 22 | local matcher = glob.glob(pattern, options) 23 | return setmetatable({ 24 | matcher = matcher 25 | }, mt) 26 | end 27 | 28 | print 'Test glob ...' 29 | test 'example' 30 | : ok 'example' 31 | : ok 'abc/example' 32 | : ok 'abc/bcd/example' 33 | : ok 'example/ddd' 34 | : ok 'abc/example/ddd' 35 | : ok 'abc/bcd/example/ddd' 36 | : ok 'abc/bcd\\example/ddd' 37 | : ok 'example/' 38 | : no 'Example' 39 | : no 'aexample' 40 | : no 'examplea' 41 | 42 | test 'example' 43 | : op 'ignoreCase' 44 | : ok 'example' 45 | : ok 'Example' 46 | : no 'aexample' 47 | 48 | test '/example' 49 | : ok 'example' 50 | : ok 'example/xx' 51 | : no 'xx/example' 52 | 53 | test '/a/b' 54 | : ok 'a/b' 55 | : ok 'a/b/c' 56 | : no 'b/a/b' 57 | 58 | test './a' 59 | : ok 'a' 60 | : ok 'a/b' 61 | : no 'b/a' 62 | 63 | test {'a', 'b'} 64 | : ok 'a' 65 | : ok 'b' 66 | : ok 'ccc/a' 67 | : ok 'ccc/b' 68 | : no 'c' 69 | 70 | test {'a','!b'} 71 | : ok 'a' 72 | : no 'b' 73 | : no 'b/a' 74 | : no 'a/b' 75 | : no 'c/b' 76 | : ok 'c/a' 77 | 78 | test {'a','!/a'} 79 | : no 'a' 80 | : ok 'b/a' 81 | 82 | test '**/a' 83 | : ok 'a' 84 | : ok 'b/a' 85 | : ok 'c/b/a' 86 | : ok 'a/b' 87 | : ok 'c/a/b' 88 | : no 'b' 89 | 90 | test '**a' 91 | : ok 'a' 92 | : ok 'b/a' 93 | : ok 'c/b/a' 94 | : ok 'a/b' 95 | : ok 'c/a/b' 96 | : no 'b' 97 | 98 | test 'a/**/b' 99 | : ok 'a/b' 100 | : ok 'a/c/b' 101 | : ok 'd/a/c/d/e/b' 102 | : no 'a' 103 | : no 'b' 104 | 105 | test 'a**b' 106 | : ok 'a/b' 107 | : ok 'a/c/b' 108 | : ok 'd/a/c/d/e/b' 109 | : no 'a' 110 | : no 'b' 111 | 112 | test 'a/**b' 113 | : ok 'a/b' 114 | : ok 'a/c/b' 115 | : ok 'd/a/c/d/e/b' 116 | : no 'a' 117 | : no 'b' 118 | 119 | test 'a**/b' 120 | : ok 'a/b' 121 | : ok 'a/c/b' 122 | : ok 'd/a/c/d/e/b' 123 | : no 'a' 124 | : no 'b' 125 | 126 | test '{a,b}' 127 | : ok 'a' 128 | : ok 'b' 129 | : ok 'c/a' 130 | : ok 'c/b' 131 | : no 'c' 132 | 133 | test 'a*' 134 | : ok 'a' 135 | : ok 'ab' 136 | : ok 'ab/c' 137 | : ok 'c/a' 138 | : ok 'c/ab' 139 | : no 'ba' 140 | : no 'bac' 141 | 142 | test 'a*b' 143 | : ok 'ab' 144 | : ok 'acb' 145 | : ok 'a/ab' 146 | : ok 'aaaabbb' 147 | : no 'abc' 148 | : no 'a/b' 149 | 150 | test 'doc/*.txt' 151 | : ok 'doc/notes.txt' 152 | : no 'doc/server/arch.txt' 153 | 154 | test {'*.a','!lib.a'} 155 | : no 'lib.a' 156 | 157 | test '{**/*.html, **/*.txt}' 158 | : ok '1.html' 159 | : ok '1.txt' 160 | : ok 'a/b.html' 161 | : ok 'a/b.txt' 162 | : no '1.lua' 163 | 164 | test 'a?b' 165 | : ok 'acb' 166 | : no 'ab' 167 | : no 'a/b' 168 | : no 'aaab' 169 | : no 'abbb' 170 | 171 | test 'example.[0-9]' 172 | : ok 'example.0' 173 | : ok 'example.5' 174 | : no 'example.' 175 | : no 'example.a' 176 | : no 'example.10' 177 | 178 | test 'example.[0-9a-z]' 179 | : ok 'example.0' 180 | : ok 'example.5' 181 | : ok 'example.a' 182 | : no 'example.' 183 | : no 'example.10' 184 | 185 | test 'example.[0-9az]' 186 | : ok 'example.0' 187 | : ok 'example.5' 188 | : ok 'example.a' 189 | : no 'example.' 190 | : no 'example.10' 191 | 192 | test {'src', '!*.dll'} 193 | : op 'ignoreCase' 194 | : ok 'Src/main.lua' 195 | : no 'Src/lpeg.dll' 196 | 197 | test 'example.\\[0-9az\\]' 198 | : no 'example.0' 199 | : no 'example.5' 200 | : no 'example.a' 201 | : no 'example.' 202 | : no 'example.10' 203 | : ok 'example.[0-9az]' 204 | 205 | test '*.lua.txt' 206 | : ok '/mnt/d/1.lua.txt' 207 | 208 | test '*.lua.txt' 209 | : ok [[D:\github\test\a.lua.txt]] 210 | 211 | test '{a, !a/b}' 212 | : ok 'a' 213 | : ok 'a/b' 214 | 215 | test 'a{1..100}' 216 | : ok 'a1' 217 | : ok 'a2' 218 | : ok 'a100' 219 | : no 'a0' 220 | : no 'a101' 221 | 222 | test 'a{a,b,c}' 223 | : ok 'aa' 224 | : ok 'ab' 225 | : ok 'ac' 226 | : no 'a' 227 | 228 | test { 229 | 'src', 230 | '!*.dll', 231 | 'lua/hello.lua', 232 | 'lua/PUB_*.lua', 233 | 'lua/PUB_', 234 | 'lua/PUB_*', 235 | 'aaa/bbb_ccc.lua', 236 | 'lua/pub2_*.lua', 237 | 'aaBBccDD.lua', 238 | } 239 | : op 'ignoreCase' 240 | : ok 'Src/main.lua' 241 | : no 'Src/lpeg.dll' 242 | : ok 'lua/hello.lua' 243 | : ok 'lua/PUB_Settings.lua' 244 | : ok 'lua/PUB_' 245 | : ok 'lua/PUB_111' 246 | : ok 'aaa/bbb_ccc.lua' 247 | : ok 'lua/pub2_bbb.lua' 248 | : ok 'aaBBccDD.lua' 249 | -------------------------------------------------------------------------------- /test/utility.lua: -------------------------------------------------------------------------------- 1 | local table_sort = table.sort 2 | local stringRep = string.rep 3 | local type = type 4 | local pairs = pairs 5 | local ipairs = ipairs 6 | local next = next 7 | local rawset = rawset 8 | local move = table.move or function(a1, f, e, t, a2) 9 | a2 = a2 or a1 10 | if e >= f then 11 | local m, n, d = 0, e-f, 1 12 | if t > f then m, n, d = n, m, -1 end 13 | for i = m, n, d do a2[t+i] = a1[f+i] end 14 | end 15 | return a2 16 | end 17 | local setmetatable = setmetatable 18 | local tableSort = table.sort 19 | local mathType = math.type 20 | 21 | local function formatNumber(n) 22 | local str = ('%.10f'):format(n) 23 | str = str:gsub('%.?0*$', '') 24 | return str 25 | end 26 | 27 | local TAB = setmetatable({}, { __index = function (self, n) 28 | self[n] = stringRep('\t', n) 29 | return self[n] 30 | end}) 31 | 32 | local RESERVED = { 33 | ['and'] = true, 34 | ['break'] = true, 35 | ['do'] = true, 36 | ['else'] = true, 37 | ['elseif'] = true, 38 | ['end'] = true, 39 | ['false'] = true, 40 | ['for'] = true, 41 | ['function'] = true, 42 | ['goto'] = true, 43 | ['if'] = true, 44 | ['in'] = true, 45 | ['local'] = true, 46 | ['nil'] = true, 47 | ['not'] = true, 48 | ['or'] = true, 49 | ['repeat'] = true, 50 | ['return'] = true, 51 | ['then'] = true, 52 | ['true'] = true, 53 | ['until'] = true, 54 | ['while'] = true, 55 | } 56 | 57 | function table.dump(tbl) 58 | if type(tbl) ~= 'table' then 59 | return ('%q'):format(tbl) 60 | end 61 | local lines = {} 62 | local mark = {} 63 | lines[#lines+1] = '{' 64 | local function unpack(tbl, tab) 65 | if mark[tbl] and mark[tbl] > 0 then 66 | lines[#lines+1] = TAB[tab+1] .. '""' 67 | return 68 | end 69 | if #lines > 10000 then 70 | lines[#lines+1] = TAB[tab+1] .. '""' 71 | return 72 | end 73 | mark[tbl] = (mark[tbl] or 0) + 1 74 | local keys = {} 75 | local keymap = {} 76 | local integerFormat = '[%d]' 77 | if #tbl >= 10 then 78 | local width = math.log(#tbl, 10) 79 | integerFormat = ('[%%0%dd]'):format(math.ceil(width)) 80 | end 81 | for key in pairs(tbl) do 82 | if type(key) == 'string' then 83 | if not key:match('^[%a_][%w_]*$') 84 | or #key >= 32 85 | or RESERVED[key] 86 | then 87 | keymap[key] = ('[%q]'):format(key) 88 | else 89 | keymap[key] = key 90 | end 91 | elseif mathType(key) == 'integer' then 92 | keymap[key] = integerFormat:format(key) 93 | else 94 | keymap[key] = ('["<%s>"]'):format(key) 95 | end 96 | keys[#keys+1] = key 97 | end 98 | local mt = getmetatable(tbl) 99 | if not mt or not mt.__pairs then 100 | tableSort(keys, function (a, b) 101 | return keymap[a] < keymap[b] 102 | end) 103 | end 104 | for _, key in ipairs(keys) do 105 | local value = tbl[key] 106 | local tp = type(value) 107 | if tp == 'table' then 108 | lines[#lines+1] = ('%s%s = {'):format(TAB[tab+1], keymap[key]) 109 | unpack(value, tab+1) 110 | lines[#lines+1] = ('%s},'):format(TAB[tab+1]) 111 | elseif tp == 'string' or tp == 'boolean' then 112 | lines[#lines+1] = ('%s%s = %q,'):format(TAB[tab+1], keymap[key], value) 113 | elseif tp == 'number' then 114 | lines[#lines+1] = ('%s%s = %s,'):format(TAB[tab+1], keymap[key], formatNumber(value)) 115 | elseif tp == 'nil' then 116 | else 117 | lines[#lines+1] = ('%s%s = %s,'):format(TAB[tab+1], keymap[key], tostring(value)) 118 | end 119 | end 120 | mark[tbl] = mark[tbl] - 1 121 | end 122 | unpack(tbl, 0) 123 | lines[#lines+1] = '}' 124 | return table.concat(lines, '\r\n') 125 | end 126 | 127 | local function sort_table(tbl) 128 | if not tbl then 129 | tbl = {} 130 | end 131 | local mt = {} 132 | local keys = {} 133 | local mark = {} 134 | local n = 0 135 | for key in next, tbl do 136 | n=n+1;keys[n] = key 137 | mark[key] = true 138 | end 139 | table_sort(keys) 140 | function mt:__newindex(key, value) 141 | rawset(self, key, value) 142 | n=n+1;keys[n] = key 143 | mark[key] = true 144 | if type(value) == 'table' then 145 | sort_table(value) 146 | end 147 | end 148 | function mt:__pairs() 149 | local list = {} 150 | local m = 0 151 | for key in next, self do 152 | if not mark[key] then 153 | m=m+1;list[m] = key 154 | end 155 | end 156 | if m > 0 then 157 | move(keys, 1, n, m+1) 158 | table_sort(list) 159 | for i = 1, m do 160 | local key = list[i] 161 | keys[i] = key 162 | mark[key] = true 163 | end 164 | n = n + m 165 | end 166 | local i = 0 167 | return function () 168 | i = i + 1 169 | local key = keys[i] 170 | return key, self[key] 171 | end 172 | end 173 | 174 | return setmetatable(tbl, mt) 175 | end 176 | 177 | function table.container(tbl) 178 | return sort_table(tbl) 179 | end 180 | 181 | function table.equal(a, b) 182 | local tp1, tp2 = type(a), type(b) 183 | if tp1 ~= tp2 then 184 | return false 185 | end 186 | if tp1 == 'table' then 187 | local mark = {} 188 | for k in pairs(a) do 189 | if not table.equal(a[k], b[k]) then 190 | return false 191 | end 192 | mark[k] = true 193 | end 194 | for k in pairs(b) do 195 | if not mark[k] then 196 | return false 197 | end 198 | end 199 | return true 200 | end 201 | return a == b 202 | end 203 | 204 | function table.deepCopy(a) 205 | local t = {} 206 | for k, v in pairs(a) do 207 | if type(v) == 'table' then 208 | t[k] = table.deepCopy(v) 209 | else 210 | t[k] = v 211 | end 212 | end 213 | return t 214 | end 215 | 216 | function io.load(file_path) 217 | local f, e = io.open(file_path:string(), 'rb') 218 | if not f then 219 | return nil, e 220 | end 221 | if f:read(3) ~= '\xEF\xBB\xBF' then 222 | f:seek("set") 223 | end 224 | local buf = f:read 'a' 225 | f:close() 226 | return buf 227 | end 228 | 229 | function io.save(file_path, content) 230 | local f, e = io.open(file_path:string(), "wb") 231 | 232 | if f then 233 | f:write(content) 234 | f:close() 235 | return true 236 | else 237 | return false, e 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /test/gitignore/scan.lua: -------------------------------------------------------------------------------- 1 | local glob = require 'glob' 2 | 3 | local EXISTS = {} 4 | local GITIGNORE = {} 5 | 6 | local ignoreCase = false 7 | 8 | local function eq(a, b) 9 | if a == EXISTS and b ~= nil then 10 | return true 11 | end 12 | local tp1, tp2 = type(a), type(b) 13 | if tp1 ~= tp2 then 14 | return false 15 | end 16 | if tp1 == 'table' then 17 | local mark = {} 18 | for k in pairs(a) do 19 | if not eq(a[k], b[k]) then 20 | return false 21 | end 22 | mark[k] = true 23 | end 24 | for k in pairs(b) do 25 | if not mark[k] then 26 | return false 27 | end 28 | end 29 | return true 30 | end 31 | return a == b 32 | end 33 | 34 | local function splitPath(path) 35 | local result = {} 36 | for name in path:gmatch '[^/\\]+' do 37 | result[#result+1] = name 38 | end 39 | return result 40 | end 41 | 42 | local function getPath(dir, path) 43 | local current = dir 44 | for name in path:gmatch '[^/\\]+' do 45 | current = current[name] 46 | if current == nil then 47 | return nil 48 | end 49 | end 50 | return current 51 | end 52 | 53 | local function buildExpect(root) 54 | local result = {} 55 | for k, v in pairs(root) do 56 | if type(v) == 'table' then 57 | result[k] = buildExpect(v) 58 | elseif v == true then 59 | result[k] = true 60 | end 61 | end 62 | if not next(result) then 63 | return nil 64 | end 65 | return result 66 | end 67 | 68 | local function test(start) 69 | return function (dir) 70 | local session = glob.gitignore(nil, { 71 | ignoreCase = ignoreCase, 72 | }) 73 | session:setInterface('type', function (path) 74 | local current = getPath(dir, path) 75 | if type(current) == 'boolean' then 76 | return 'file' 77 | elseif type(current) == 'table' then 78 | return 'directory' 79 | end 80 | return nil 81 | end) 82 | session:setInterface('list', function (path) 83 | local current = getPath(dir, path) 84 | if type(current) == 'table' then 85 | local childs = {} 86 | for name in pairs(current) do 87 | if type(name) ~= 'string' then 88 | goto continue 89 | end 90 | if path == '' then 91 | childs[#childs+1] = name 92 | else 93 | childs[#childs+1] = path .. '/' .. name 94 | end 95 | ::continue:: 96 | end 97 | table.sort(childs) 98 | return childs 99 | end 100 | return nil 101 | end) 102 | session:setInterface('patterns', function (path) 103 | local current = getPath(dir, path) 104 | if type(current) == 'table' and current[GITIGNORE] then 105 | return current[GITIGNORE] 106 | end 107 | return nil 108 | end) 109 | 110 | local result = {} 111 | session:scan(start, function (path) 112 | local current = result 113 | local names = splitPath(path) 114 | for i = 1, #names-1 do 115 | local name = names[i] 116 | if not current[name] then 117 | current[name] = {} 118 | end 119 | current = current[name] 120 | end 121 | local name = names[#names] 122 | current[name] = true 123 | end) 124 | 125 | local expect = buildExpect(dir) 126 | assert(eq(expect, result)) 127 | end 128 | end 129 | 130 | test '' 131 | { 132 | [GITIGNORE] = { 133 | '*.dll', 134 | }, 135 | ['a.lua'] = true, 136 | ['b.lua'] = true, 137 | ['c.dll'] = false, 138 | ['bin'] = { 139 | ['a.dll'] = false, 140 | ['b.dll'] = false, 141 | }, 142 | ['src'] = { 143 | ['a.lua'] = true, 144 | ['b.lua'] = true, 145 | ['c.lua'] = true, 146 | ['d.dll'] = false, 147 | } 148 | } 149 | 150 | test '' 151 | { 152 | [GITIGNORE] = { 153 | '*.*', 154 | '!*.lua', 155 | }, 156 | ['a.lua'] = true, 157 | ['b.lua'] = true, 158 | ['c.dll'] = false, 159 | ['bin'] = { 160 | ['a.dll'] = false, 161 | ['b.dll'] = false, 162 | }, 163 | ['src'] = { 164 | ['a.lua'] = true, 165 | ['b.lua'] = true, 166 | ['c.lua'] = true, 167 | ['d.dll'] = false, 168 | } 169 | } 170 | 171 | test '' 172 | { 173 | [GITIGNORE] = { 174 | '/*.dll' 175 | }, 176 | ['a.lua'] = true, 177 | ['b.lua'] = true, 178 | ['c.dll'] = false, 179 | ['bin'] = { 180 | ['a.dll'] = true, 181 | ['b.dll'] = true, 182 | }, 183 | ['src'] = { 184 | ['a.lua'] = true, 185 | ['b.lua'] = true, 186 | ['c.lua'] = true, 187 | ['d.dll'] = true, 188 | } 189 | } 190 | 191 | test '' 192 | { 193 | [GITIGNORE] = { 194 | 'src/*', 195 | '!*.dll', 196 | }, 197 | ['a.lua'] = true, 198 | ['b.lua'] = true, 199 | ['c.dll'] = true, 200 | ['bin'] = { 201 | ['a.dll'] = true, 202 | ['b.dll'] = true, 203 | }, 204 | ['src'] = { 205 | ['a.lua'] = false, 206 | ['b.lua'] = false, 207 | ['c.lua'] = false, 208 | ['d.dll'] = true, 209 | } 210 | } 211 | 212 | test '' 213 | { 214 | [GITIGNORE] = { 215 | '*.lua', 216 | '!/*.lua', 217 | }, 218 | ['a.lua'] = true, 219 | ['publish'] = { 220 | ['a.lua'] = false, 221 | } 222 | } 223 | 224 | test 'root' 225 | { 226 | ['root'] = { 227 | [GITIGNORE] = { 228 | '/*', 229 | '!/usr', 230 | }, 231 | ['a.lua'] = false, 232 | ['usr'] = { 233 | ['a.lua'] = true, 234 | } 235 | } 236 | } 237 | 238 | test 'root' 239 | { 240 | ['root'] = { 241 | [GITIGNORE] = { 242 | 'XXX/YYY', 243 | }, 244 | ['a.lua'] = true, 245 | ['b.lua'] = true, 246 | ['XXX'] = { 247 | ['a.lua'] = true, 248 | ['b.lua'] = true, 249 | ['YYY'] = { 250 | ['a.lua'] = false, 251 | ['b.lua'] = false, 252 | ['ZZZ'] = { 253 | ['a.lua'] = false, 254 | ['b.lua'] = false, 255 | } 256 | }, 257 | } 258 | } 259 | } 260 | 261 | test 'root' 262 | { 263 | [GITIGNORE] = { 264 | 'b.lua', 265 | '/YYY', 266 | 'ZZZ', 267 | }, 268 | ['root'] = { 269 | ['a.lua'] = true, 270 | ['b.lua'] = false, 271 | ['c.lua'] = true, 272 | ['XXX'] = { 273 | [GITIGNORE] = { 274 | 'a.lua', 275 | '/c.lua', 276 | }, 277 | ['a.lua'] = false, 278 | ['b.lua'] = false, 279 | ['c.lua'] = false, 280 | ['YYY'] = { 281 | ['a.lua'] = false, 282 | ['b.lua'] = false, 283 | ['c.lua'] = true, 284 | ['ZZZ'] = { 285 | ['a.lua'] = false, 286 | ['b.lua'] = false, 287 | ['c.lua'] = false, 288 | } 289 | }, 290 | }, 291 | ['YYY'] = { 292 | ['a.lua'] = true, 293 | ['b.lua'] = false, 294 | ['c.lua'] = true, 295 | }, 296 | ['ZZZ'] = { 297 | ['a.lua'] = false, 298 | ['b.lua'] = false, 299 | ['c.lua'] = false, 300 | } 301 | }, 302 | ['YYY'] = { 303 | ['a.lua'] = false, 304 | ['b.lua'] = false, 305 | ['c.lua'] = false, 306 | } 307 | } 308 | 309 | ignoreCase = true 310 | test 'root' 311 | { 312 | [GITIGNORE] = { 313 | 'b.Lua', 314 | '/yYy', 315 | 'zZz', 316 | }, 317 | ['root'] = { 318 | ['A.lua'] = true, 319 | ['B.lua'] = false, 320 | ['C.lua'] = true, 321 | ['XXX'] = { 322 | [GITIGNORE] = { 323 | 'A.lua', 324 | '/C.Lua', 325 | }, 326 | ['A.lua'] = false, 327 | ['B.lua'] = false, 328 | ['C.lua'] = false, 329 | ['YYY'] = { 330 | ['A.lua'] = false, 331 | ['B.lua'] = false, 332 | ['C.lua'] = true, 333 | ['ZZZ'] = { 334 | ['A.lua'] = false, 335 | ['B.lua'] = false, 336 | ['C.lua'] = false, 337 | } 338 | }, 339 | }, 340 | ['YYY'] = { 341 | ['A.lua'] = true, 342 | ['B.lua'] = false, 343 | ['C.lua'] = true, 344 | }, 345 | ['ZZZ'] = { 346 | ['A.lua'] = false, 347 | ['B.lua'] = false, 348 | ['C.lua'] = false, 349 | } 350 | }, 351 | ['YYY'] = { 352 | ['A.lua'] = false, 353 | ['B.lua'] = false, 354 | ['C.lua'] = false, 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /glob.lua: -------------------------------------------------------------------------------- 1 | ---@class Glob 2 | local M = {} 3 | M.__index = M 4 | 5 | ---@class Glob.Options 6 | ---@field ignoreCase? boolean 7 | ---@field asGitIgnore? boolean 8 | ---@field root? string 9 | 10 | ---@class Glob.Interface 11 | ---@field type? fun(path: string):('file'|'directory'|nil) 12 | ---@field list? fun(path: string):string[]? 13 | ---@field patterns? fun(path: string):string[]? 14 | 15 | local function parsePatternRangePart(pat, start) 16 | local pack = { 17 | kind = 'range', 18 | ranges = {}, 19 | singles = {}, 20 | } 21 | local index = start 22 | while index <= #pat do 23 | local char = pat:sub(index, index) 24 | index = index + 1 25 | if char == ']' then 26 | return pack, index 27 | end 28 | if char == '\\' then 29 | char = pat:sub(index, index) 30 | index = index + 1 31 | end 32 | local peek = pat:sub(index, index) 33 | if peek == '-' then 34 | local stop = pat:sub(index + 1, index + 1) 35 | pack.ranges[#pack.ranges+1] = { char, stop } 36 | index = index + 2 37 | else 38 | pack.singles[#pack.singles+1] = char 39 | end 40 | end 41 | return pack, index 42 | end 43 | 44 | local function parsePatternBracePart(pat, start) 45 | local s, e, a1, a2 = pat:find('(%a)%.%.(%a)%}', start) 46 | if s then 47 | if a1 > a2 then 48 | a1, a2 = a2, a1 49 | end 50 | return { 51 | kind = 'brace', 52 | alphaRange = { a1, a2 }, 53 | }, e + 1 54 | end 55 | local s, e, n1, n2 = pat:find('(%d+)%.%.(%d+)%}', start) 56 | if s then 57 | n1 = tonumber(n1) 58 | n2 = tonumber(n2) 59 | if n1 > n2 then 60 | n1, n2 = n2, n1 61 | end 62 | return { 63 | kind = 'brace', 64 | numberRange = { n1, n2 }, 65 | }, e + 1 66 | end 67 | local pack = { 68 | kind = 'brace', 69 | singles = {}, 70 | } 71 | local index = start 72 | local buf = '' 73 | while index <= #pat do 74 | local pos, _, char = pat:find('([%,%}%\\])', index) 75 | if not pos then 76 | buf = buf .. pat:sub(index) 77 | if #buf > 0 then 78 | pack.singles[#pack.singles+1] = buf 79 | end 80 | index = #pat + 1 81 | break 82 | end 83 | if pos > index then 84 | buf = buf .. pat:sub(index, pos - 1) 85 | end 86 | index = pos + 1 87 | if char == '}' then 88 | if #buf > 0 then 89 | pack.singles[#pack.singles+1] = buf 90 | end 91 | return pack, index 92 | elseif char == ',' then 93 | if #buf > 0 then 94 | pack.singles[#pack.singles+1] = buf 95 | end 96 | buf = '' 97 | elseif char == '\\' then 98 | buf = buf .. pat:sub(index, index) 99 | index = index + 1 100 | else 101 | buf = buf .. char 102 | end 103 | end 104 | return pack, index 105 | end 106 | 107 | ---@private 108 | ---@param pat string 109 | ---@return table 110 | ---@return integer 111 | function M:parsePatternOr(pat) 112 | local result = { 113 | kind = 'or', 114 | childs = {}, 115 | } 116 | 117 | pat = pat:sub(2) 118 | while true do 119 | local pattern, pos = self:parsePattern(pat) 120 | if not pattern then 121 | break 122 | end 123 | result.childs[#result.childs+1] = pattern 124 | local char = pat:sub(pos, pos) 125 | if char == ',' then 126 | pat = pat:sub(pos + 1) 127 | elseif char == '}' then 128 | return result, pos + 1 129 | else 130 | return result, pos 131 | end 132 | end 133 | 134 | return result, #pat + 1 135 | end 136 | 137 | ---@private 138 | ---@param a string 139 | ---@param b string 140 | ---@return boolean 141 | function M:pathEqual(a, b) 142 | if self.options.ignoreCase then 143 | return a:lower() == b:lower() 144 | end 145 | return a == b 146 | end 147 | 148 | ---@private 149 | ---@param pat string 150 | ---@return table 151 | ---@return integer 152 | function M:parsePattern(pat) 153 | if pat:sub(1, 1) == '{' then 154 | return self:parsePatternOr(pat) 155 | end 156 | 157 | local result = { 158 | kind = 'base', 159 | symbols = {}, 160 | } 161 | 162 | pat = pat:gsub('^%s+', ''):gsub('%s+$', '') 163 | local last = 1 164 | if pat:sub(last, last) == '!' then 165 | result.refused = true 166 | last = last + 1 167 | end 168 | if pat:sub(last, last) == '/' then 169 | result.root = true 170 | last = last + 1 171 | elseif pat:sub(last, last + 1) == './' then 172 | result.root = true 173 | last = last + 2 174 | end 175 | if pat:sub(-1) == '/' then 176 | result.onlyDirectory = true 177 | pat = pat:sub(1, #pat - 1) 178 | end 179 | while true do 180 | local start, _, char = pat:find('([%*%?%[%/%\\%{%,%}])', last) 181 | if not start then 182 | if last <= #pat then 183 | result.symbols[#result.symbols+1] = pat:sub(last) 184 | end 185 | return result, #pat 186 | end 187 | if start > last then 188 | result.symbols[#result.symbols+1] = pat:sub(last, start - 1) 189 | end 190 | if char == '*' then 191 | if pat:sub(start + 1, start + 1) == '*' then 192 | -- ** 193 | result.symbols[#result.symbols+1] = '**' 194 | last = start + 2 195 | if pat:sub(last, last) == '/' then 196 | last = last + 1 197 | end 198 | else 199 | result.symbols[#result.symbols+1] = '*' 200 | last = start + 1 201 | end 202 | elseif char == '?' or char == '/' then 203 | result.symbols[#result.symbols+1] = char 204 | last = start + 1 205 | elseif char == '[' then 206 | result.symbols[#result.symbols+1], last = parsePatternRangePart(pat, start + 1) 207 | elseif char == '{' then 208 | result.symbols[#result.symbols+1], last = parsePatternBracePart(pat, start + 1) 209 | elseif char == '\\' then 210 | result.symbols[#result.symbols+1] = pat:sub(start + 1, start + 1) 211 | last = start + 2 212 | elseif char == ',' or char == '}' then 213 | return result, start 214 | end 215 | end 216 | end 217 | 218 | ---@param pat string 219 | ---@param base? string 220 | function M:addPattern(pat, base) 221 | base = base or '' 222 | if self.options.ignoreCase then 223 | base = base:lower() 224 | end 225 | if not self.patternMap[base] then 226 | self.patternMap[base] = {} 227 | end 228 | local pattern = self:parsePattern(pat) 229 | table.insert(self.patternMap[base], pattern) 230 | end 231 | 232 | ---@param op string 233 | ---@param val any 234 | function M:setOption(op, val) 235 | if val == nil then 236 | val = true 237 | end 238 | self.options[op] = val 239 | end 240 | 241 | ---@overload fun(self: Glob, key: 'type', func: fun(path: string):('file'|'directory'|nil)) 242 | ---@overload fun(self: Glob, key: 'list', func: fun(path: string):string[]?) 243 | ---@overload fun(self: Glob, key: 'patterns', func: fun(path: string):string[]?) 244 | function M:setInterface(key, func) 245 | if type(func) ~= 'function' then 246 | return 247 | end 248 | self.interface[key] = func 249 | end 250 | 251 | ---@private 252 | function M:checkPatternByBrace(path, pat) 253 | if pat.singles then 254 | for _, single in ipairs(pat.singles) do 255 | if self:pathEqual(path:sub(1, #single), single) then 256 | return true, path:sub(#single + 1) 257 | end 258 | end 259 | return false 260 | end 261 | if pat.alphaRange then 262 | local first = pat.alphaRange[1] 263 | local last = pat.alphaRange[2] 264 | if self.options.ignoreCase then 265 | first = first:lower() 266 | last = last:lower() 267 | end 268 | if path:sub(1, 1) >= first and path:sub(1, 1) <= last then 269 | return true, path:sub(2) 270 | end 271 | return false 272 | end 273 | if pat.numberRange then 274 | local first = pat.numberRange[1] 275 | local last = pat.numberRange[2] 276 | for i = #tostring(last), #tostring(first), -1 do 277 | local char = path:sub(1, i) 278 | local num = tonumber(char) 279 | if num and num >= first and num <= last then 280 | return true, path:sub(i + 1) 281 | end 282 | end 283 | return false 284 | end 285 | end 286 | 287 | ---@private 288 | function M:checkPatternByRange(path, pat) 289 | local ignoreCase = self.options.ignoreCase 290 | local char = path:sub(1, 1) 291 | for _, range in ipairs(pat.ranges) do 292 | local s, e = range[1], range[2] 293 | if ignoreCase then 294 | s = s:lower() 295 | e = e:lower() 296 | end 297 | if char >= s and char <= e then 298 | return true, path:sub(2) 299 | end 300 | end 301 | for _, single in ipairs(pat.singles) do 302 | if ignoreCase then 303 | single = single:lower() 304 | end 305 | if char == single then 306 | return true, path:sub(2) 307 | end 308 | end 309 | return false 310 | end 311 | 312 | ---@private 313 | function M:checkPatternWord(path, pattern, patIndex) 314 | for i = patIndex, #pattern.symbols do 315 | local pat = pattern.symbols[i] 316 | if pat == '*' then 317 | if path == nil then 318 | return false 319 | end 320 | if path == '' then 321 | return true, i + 1 322 | end 323 | local newPath = path 324 | while true do 325 | local suc, newIndex = self:checkPatternWord(newPath, pattern, i + 1) 326 | if suc then 327 | return true, newIndex 328 | end 329 | if newPath == '' then 330 | return false 331 | end 332 | newPath = newPath:sub(2) 333 | end 334 | return false 335 | elseif pat == '?' then 336 | if path == nil or path == '' then 337 | return false 338 | end 339 | path = path:sub(2) 340 | elseif pat == '/' or pat == '**' then 341 | if path == nil or path == '' then 342 | return true, i 343 | else 344 | return false 345 | end 346 | elseif path == nil then 347 | return false 348 | elseif type(pat) == 'string' then 349 | if not self:pathEqual(path:sub(1, #pat), pat) then 350 | return false 351 | end 352 | path = path:sub(#pat + 1) 353 | elseif pat.kind == 'brace' then 354 | local ok, leftPath = self:checkPatternByBrace(path, pat) 355 | if not ok then 356 | return false 357 | end 358 | path = leftPath 359 | elseif pat.kind == 'range' then 360 | local ok, leftPath = self:checkPatternByRange(path, pat) 361 | if not ok then 362 | return false 363 | end 364 | path = leftPath 365 | end 366 | end 367 | if path == nil or path == '' then 368 | return true, #pattern.symbols + 1 369 | end 370 | return false 371 | end 372 | 373 | ---@private 374 | ---@param paths string[] 375 | ---@param pathIndex integer 376 | ---@param pattern table 377 | ---@param patIndex integer 378 | ---@return boolean 379 | function M:checkPatternSlice(paths, pathIndex, pattern, patIndex) 380 | local path = paths[pathIndex] 381 | local pat = pattern.symbols[patIndex] 382 | if pat == nil then 383 | if pattern.onlyDirectory then 384 | if pathIndex < #paths then 385 | return true 386 | end 387 | if self.interface.type then 388 | local currentPath = table.concat(paths, '/', 1, pathIndex) 389 | local fileType = self.interface.type(currentPath) 390 | if fileType ~= 'directory' then 391 | return false 392 | end 393 | end 394 | end 395 | return true 396 | end 397 | if path == nil and pat == nil then 398 | return true 399 | end 400 | if pat == '/' then 401 | return self:checkPatternSlice(paths, pathIndex + 1, pattern, patIndex + 1) 402 | end 403 | if pat == '**' then 404 | for i = pathIndex, #paths do 405 | if self:checkPatternSlice(paths, i, pattern, patIndex + 1) then 406 | return true 407 | end 408 | end 409 | return false 410 | end 411 | local ok, newIndex = self:checkPatternWord(path, pattern, patIndex) 412 | if not ok then 413 | return false 414 | end 415 | ---@cast newIndex -? 416 | return self:checkPatternSlice(paths, pathIndex, pattern, newIndex) 417 | end 418 | 419 | ---@private 420 | ---@param paths string[] 421 | ---@param pattern table 422 | ---@param start integer 423 | ---@return boolean 424 | function M:checkPattern(paths, pattern, start) 425 | if pattern.kind == 'base' then 426 | if #pattern.symbols == 0 then 427 | return false 428 | end 429 | if pattern.root then 430 | return self:checkPatternSlice(paths, start, pattern, 1) 431 | else 432 | for i = start, #paths do 433 | if self:checkPatternSlice(paths, i, pattern, 1) then 434 | return true 435 | end 436 | end 437 | return false 438 | end 439 | elseif pattern.kind == 'or' then 440 | for _, child in ipairs(pattern.childs) do 441 | if self:checkPattern(paths, child, start) then 442 | return true 443 | end 444 | end 445 | return false 446 | end 447 | return false 448 | end 449 | 450 | ---@param paths string[] 451 | ---@return 'accepted'|'refused'|'unknown' 452 | function M:status(paths) 453 | local status = 'unknown' 454 | 455 | local function statusWithPatterns(patterns, start) 456 | for _, pattern in ipairs(patterns) do 457 | if status ~= 'refused' and pattern.refused then 458 | if self:checkPattern(paths, pattern, start) then 459 | status = 'refused' 460 | end 461 | elseif status ~= 'accepted' and not pattern.refused then 462 | if self:checkPattern(paths, pattern, start) then 463 | status = 'accepted' 464 | end 465 | end 466 | end 467 | end 468 | 469 | if self.options.asGitIgnore then 470 | for i = 1, #paths do 471 | local base = table.concat(paths, '/', 1, i - 1) 472 | if self.options.root then 473 | base = self.options.root / base 474 | end 475 | local patternKey = base 476 | if self.options.ignoreCase then 477 | patternKey = patternKey:lower() 478 | end 479 | local patterns = self.patternMap[patternKey] 480 | if not patterns then 481 | patterns = {} 482 | self.patternMap[patternKey] = patterns 483 | if self.interface.patterns then 484 | local newPatterns = self.interface.patterns(base) 485 | if type(newPatterns) == 'table' then 486 | for _, pat in ipairs(newPatterns) do 487 | self:addPattern(pat, base) 488 | end 489 | end 490 | end 491 | end 492 | if #patterns > 0 then 493 | statusWithPatterns(patterns, i) 494 | end 495 | end 496 | else 497 | local patterns = self.patternMap[''] or {} 498 | statusWithPatterns(patterns, 1) 499 | end 500 | 501 | return status 502 | end 503 | 504 | ---@param path string 505 | ---@return boolean 506 | function M:check(path) 507 | local root = self.options.root 508 | if root then 509 | if self:pathEqual(path:sub(1, #root), root) then 510 | path = path:sub(#root + 1) 511 | else 512 | return false 513 | end 514 | end 515 | 516 | if self.options.asGitIgnore then 517 | local paths = {} 518 | for p in path:gmatch('[^/\\]+') do 519 | paths[#paths+1] = p 520 | local status = self:status(paths) 521 | if status == 'accepted' then 522 | return true 523 | elseif status == 'refused' then 524 | return false 525 | end 526 | end 527 | return false 528 | else 529 | local paths = {} 530 | for p in path:gmatch('[^/\\]+') do 531 | paths[#paths+1] = p 532 | end 533 | 534 | return self:status(paths) == 'accepted' 535 | end 536 | end 537 | 538 | ---@private 539 | ---@param path string 540 | ---@return string[] 541 | function M:toPaths(path) 542 | local root = self.options.root 543 | if root then 544 | if self:pathEqual(path:sub(1, #root), root) then 545 | path = path:sub(#root + 1) 546 | else 547 | return {} 548 | end 549 | end 550 | 551 | local paths = {} 552 | for p in path:gmatch('[^/\\]+') do 553 | paths[#paths+1] = p 554 | end 555 | return paths 556 | end 557 | 558 | ---@param root? string 559 | ---@param callback? fun(path: string) 560 | function M:scan(root, callback) 561 | root = root or '' 562 | local checked = {} 563 | 564 | local function check(path) 565 | local checkedPath = path 566 | if self.options.ignoreCase then 567 | checkedPath = checkedPath:lower() 568 | end 569 | if checked[checkedPath] then 570 | return 571 | end 572 | checked[checkedPath] = true 573 | if #path ~= root and self:status(self:toPaths(path)) == 'accepted' then 574 | return 575 | end 576 | local ftype 577 | if #path == root then 578 | ftype = 'directory' 579 | else 580 | ftype = self.interface.type and self.interface.type(path) or nil 581 | end 582 | if not ftype then 583 | return 584 | end 585 | if ftype == 'file' then 586 | if callback then 587 | callback(path) 588 | end 589 | return 590 | end 591 | if ftype == 'directory' then 592 | local files = self.interface.list and self.interface.list(path) or nil 593 | if type(files) ~= 'table' then 594 | return 595 | end 596 | for _, file in ipairs(files) do 597 | check(file) 598 | end 599 | end 600 | end 601 | 602 | check(root) 603 | end 604 | 605 | function M:__call(...) 606 | return self:check(...) 607 | end 608 | 609 | ---@param pattern? string|string[] 610 | ---@param options? Glob.Options 611 | ---@param interface? Glob.Interface 612 | ---@return Glob 613 | local function createGlob(pattern, options, interface) 614 | ---@class Glob 615 | local glob = setmetatable({ 616 | ---@type table 617 | patternMap = {}, 618 | ---@type Glob.Options 619 | options = {}, 620 | ---@type Glob.Interface 621 | interface = {}, 622 | }, M) 623 | 624 | if type(pattern) == 'table' then 625 | for _, pat in ipairs(pattern) do 626 | glob:addPattern(pat) 627 | end 628 | elseif pattern then 629 | glob:addPattern(pattern) 630 | end 631 | 632 | if type(options) == 'table' then 633 | for op, val in pairs(options) do 634 | glob:setOption(op, val) 635 | end 636 | end 637 | 638 | if type(interface) == 'table' then 639 | for key, func in pairs(interface) do 640 | glob:setInterface(key, func) 641 | end 642 | end 643 | 644 | return glob 645 | end 646 | 647 | ---@param pattern? string|string[] 648 | ---@param options? Glob.Options 649 | ---@param interface? Glob.Interface 650 | ---@return Glob 651 | local function createGitIgnore(pattern, options, interface) 652 | local glob = createGlob(pattern, options, interface) 653 | glob:setOption('asGitIgnore', true) 654 | return glob 655 | end 656 | 657 | return { 658 | glob = createGlob, 659 | gitignore = createGitIgnore, 660 | } 661 | --------------------------------------------------------------------------------