match.odin 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. package slashpath
  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. in_range or_break scan_loop
  87. }
  88. }
  89. return star, pattern[:i], pattern[i:]
  90. }
  91. @(private="file")
  92. match_chunk :: proc(chunk, s: string) -> (rest: string, ok: bool, err: Match_Error) {
  93. chunk, s := chunk, s
  94. for len(chunk) > 0 {
  95. if len(s) == 0 {
  96. return
  97. }
  98. switch chunk[0] {
  99. case '[':
  100. r, w := utf8.decode_rune_in_string(s)
  101. s = s[w:]
  102. chunk = chunk[1:]
  103. is_negated := false
  104. if len(chunk) > 0 && chunk[0] == '^' {
  105. is_negated = true
  106. chunk = chunk[1:]
  107. }
  108. match := false
  109. range_count := 0
  110. for {
  111. if len(chunk) > 0 && chunk[0] == ']' && range_count > 0 {
  112. chunk = chunk[1:]
  113. break
  114. }
  115. lo, hi: rune
  116. if lo, chunk, err = get_escape(chunk); err != .None {
  117. return
  118. }
  119. hi = lo
  120. if chunk[0] == '-' {
  121. if hi, chunk, err = get_escape(chunk[1:]); err != .None {
  122. return
  123. }
  124. }
  125. if lo <= r && r <= hi {
  126. match = true
  127. }
  128. range_count += 1
  129. }
  130. if match == is_negated {
  131. return
  132. }
  133. case '?':
  134. if s[0] == '/' {
  135. return
  136. }
  137. _, w := utf8.decode_rune_in_string(s)
  138. s = s[w:]
  139. chunk = chunk[1:]
  140. case '\\':
  141. chunk = chunk[1:]
  142. if len(chunk) == 0 {
  143. err = .Syntax_Error
  144. return
  145. }
  146. fallthrough
  147. case:
  148. if chunk[0] != s[0] {
  149. return
  150. }
  151. s = s[1:]
  152. chunk = chunk[1:]
  153. }
  154. }
  155. return s, true, .None
  156. }
  157. @(private="file")
  158. get_escape :: proc(chunk: string) -> (r: rune, next_chunk: string, err: Match_Error) {
  159. if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
  160. err = .Syntax_Error
  161. return
  162. }
  163. chunk := chunk
  164. if chunk[0] == '\\' {
  165. chunk = chunk[1:]
  166. if len(chunk) == 0 {
  167. err = .Syntax_Error
  168. return
  169. }
  170. }
  171. w: int
  172. r, w = utf8.decode_rune_in_string(chunk)
  173. if r == utf8.RUNE_ERROR && w == 1 {
  174. err = .Syntax_Error
  175. }
  176. next_chunk = chunk[w:]
  177. if len(next_chunk) == 0 {
  178. err = .Syntax_Error
  179. }
  180. return
  181. }