match.odin 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package path
  2. import "core:strings"
  3. import "core:unicode/utf8"
  4. Match_Error :: enum {
  5. None,
  6. Syntax_Error,
  7. }
  8. // match states whether "name" matches the shell pattern
  9. // Pattern syntax is:
  10. // pattern:
  11. // {term}
  12. // term:
  13. // '*' matches any sequence of non-/ characters
  14. // '?' matches any single non-/ character
  15. // '[' ['^'] { character-range } ']'
  16. // character classification (cannot be empty)
  17. // c matches character c (c != '*', '?', '\\', '[')
  18. // '\\' c matches character c
  19. //
  20. // character-range
  21. // c matches character c (c != '\\', '-', ']')
  22. // '\\' c matches character c
  23. // lo '-' hi matches character c for lo <= c <= hi
  24. //
  25. // match requires that the pattern matches the entirety of the name, not just a substring
  26. // The only possible error returned is .Syntax_Error
  27. //
  28. match :: proc(pattern, name: string) -> (matched: bool, err: Match_Error) {
  29. pattern, name := pattern, name
  30. pattern_loop: for len(pattern) > 0 {
  31. star: bool
  32. chunk: string
  33. star, chunk, pattern = scan_chunk(pattern)
  34. if star && chunk == "" {
  35. return !strings.contains(name, "/"), .None
  36. }
  37. t: string
  38. ok: bool
  39. t, ok, err = match_chunk(chunk, name)
  40. if ok && (len(t) == 0 || len(pattern) > 0) {
  41. name = t
  42. continue
  43. }
  44. if err != .None {
  45. return
  46. }
  47. if star {
  48. for i := 0; i < len(name) && name[i] != '/'; i += 1 {
  49. t, ok, err = match_chunk(chunk, name[i+1:])
  50. if ok {
  51. if len(pattern) == 0 && len(t) > 0 {
  52. continue
  53. }
  54. name = t
  55. continue pattern_loop
  56. }
  57. if err != .None {
  58. return
  59. }
  60. }
  61. }
  62. return false, .None
  63. }
  64. return len(name) == 0, .None
  65. }
  66. @(private="file")
  67. scan_chunk :: proc(pattern: string) -> (star: bool, chunk, rest: string) {
  68. pattern := pattern
  69. for len(pattern) > 0 && pattern[0] == '*' {
  70. pattern = pattern[1:]
  71. star = true
  72. }
  73. in_range := false
  74. i: int
  75. scan_loop: for i = 0; i < len(pattern); i += 1 {
  76. switch pattern[i] {
  77. case '\\':
  78. if i+1 < len(pattern) {
  79. i += 1
  80. }
  81. case '[':
  82. in_range = true
  83. case ']':
  84. in_range = false
  85. case '*':
  86. if !in_range {
  87. break scan_loop
  88. }
  89. }
  90. }
  91. return star, pattern[:i], pattern[i:]
  92. }
  93. @(private="file")
  94. match_chunk :: proc(chunk, s: string) -> (rest: string, ok: bool, err: Match_Error) {
  95. chunk, s := chunk, s
  96. for len(chunk) > 0 {
  97. if len(s) == 0 {
  98. return
  99. }
  100. switch chunk[0] {
  101. case '[':
  102. r, w := utf8.decode_rune_in_string(s)
  103. s = s[w:]
  104. chunk = chunk[1:]
  105. is_negated := false
  106. if len(chunk) > 0 && chunk[0] == '^' {
  107. is_negated = true
  108. chunk = chunk[1:]
  109. }
  110. match := false
  111. range_count := 0
  112. for {
  113. if len(chunk) > 0 && chunk[0] == ']' && range_count > 0 {
  114. chunk = chunk[1:]
  115. break
  116. }
  117. lo, hi: rune
  118. if lo, chunk, err = get_escape(chunk); err != .None {
  119. return
  120. }
  121. hi = lo
  122. if chunk[0] == '-' {
  123. if hi, chunk, err = get_escape(chunk[1:]); err != .None {
  124. return
  125. }
  126. }
  127. if lo <= r && r <= hi {
  128. match = true
  129. }
  130. range_count += 1
  131. }
  132. if match == is_negated {
  133. return
  134. }
  135. case '?':
  136. if s[0] == '/' {
  137. return
  138. }
  139. _, w := utf8.decode_rune_in_string(s)
  140. s = s[w:]
  141. chunk = chunk[1:]
  142. case '\\':
  143. chunk = chunk[1:]
  144. if len(chunk) == 0 {
  145. err = .Syntax_Error
  146. return
  147. }
  148. fallthrough
  149. case:
  150. if chunk[0] != s[0] {
  151. return
  152. }
  153. s = s[1:]
  154. chunk = chunk[1:]
  155. }
  156. }
  157. return s, true, .None
  158. }
  159. @(private="file")
  160. get_escape :: proc(chunk: string) -> (r: rune, next_chunk: string, err: Match_Error) {
  161. if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
  162. err = .Syntax_Error
  163. return
  164. }
  165. chunk := chunk
  166. if chunk[0] == '\\' {
  167. chunk = chunk[1:]
  168. if len(chunk) == 0 {
  169. err = .Syntax_Error
  170. return
  171. }
  172. }
  173. w: int
  174. r, w = utf8.decode_rune_in_string(chunk)
  175. if r == utf8.RUNE_ERROR && w == 1 {
  176. err = .Syntax_Error
  177. }
  178. next_chunk = chunk[w:]
  179. if len(next_chunk) == 0 {
  180. err = .Syntax_Error
  181. }
  182. return
  183. }