github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/cmn/cos/template.go (about) 1 // Package cos provides common low-level types and utilities for all aistore projects 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package cos 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "math" 12 "strconv" 13 "strings" 14 "unicode" 15 ) 16 17 const ( 18 WildcardMatchAll = "*" 19 EmptyMatchAll = "" 20 ) 21 22 func MatchAll(template string) bool { return template == EmptyMatchAll || template == WildcardMatchAll } 23 24 // Supported syntax includes 3 standalone variations, 3 alternative formats: 25 // 1. bash (or shell) brace expansion: 26 // * `prefix-{0..100}-suffix` 27 // * `prefix-{00001..00010..2}-gap-{001..100..2}-suffix` 28 // 2. at style: 29 // * `prefix-@100-suffix` 30 // * `prefix-@00001-gap-@100-suffix` 31 // 3. fmt style: 32 // * `prefix-%06d-suffix` 33 // In all cases, prefix and/or suffix are optional. 34 // 35 // NOTE: if none of the above applies, `NewParsedTemplate()` simply returns 36 // `ParsedTemplate{Prefix = original template string}` with nil Ranges 37 38 type ( 39 TemplateRange struct { 40 Gap string // characters after the range (to the next range or end of the string) 41 Start int64 42 End int64 43 Step int64 44 DigitCount int 45 } 46 ParsedTemplate struct { 47 Prefix string 48 Ranges []TemplateRange 49 at []int64 50 buf bytes.Buffer 51 rangesCount int 52 } 53 54 errTemplateInvalid struct { 55 msg string 56 } 57 ) 58 59 const ( 60 invalidFmt = "invalid 'fmt' template %q (expecting e.g. 'prefix-%%06d-suffix)" 61 invalidBash = "invalid 'bash' template %q (expecting e.g. 'prefix-{0001..0010..1}-suffix')" 62 invalidAt = "invalid 'at' template %q (expecting e.g. 'prefix-@00100-suffix')" 63 startAfterEnd = "invalid '%s' template %q: 'start' cannot be greater than 'end'" 64 negativeStart = "invalid '%s' template %q: 'start' is negative" 65 nonPositiveStep = "invalid '%s' template %q: 'step' is non-positive" 66 ) 67 68 var ( 69 ErrEmptyTemplate = errors.New("empty range template") 70 71 errTemplateNotBash = errors.New("not a 'bash' template") 72 errTemplateNotFmt = errors.New("not an 'fmt' template") 73 errTemplateNotAt = errors.New("not an 'at' template") 74 ) 75 76 func newErrTemplateInvalid(efmt string, a ...any) error { 77 return &errTemplateInvalid{fmt.Sprintf(efmt, a...)} 78 } 79 func (e *errTemplateInvalid) Error() string { return e.msg } 80 81 //////////////////// 82 // ParsedTemplate // 83 //////////////////// 84 85 func NewParsedTemplate(template string) (parsed ParsedTemplate, err error) { 86 if MatchAll(template) { 87 err = ErrEmptyTemplate 88 return 89 } 90 91 parsed, err = ParseBashTemplate(template) 92 if err == nil || err != errTemplateNotBash { 93 return 94 } 95 parsed, err = ParseAtTemplate(template) 96 if err == nil || err != errTemplateNotAt { 97 return 98 } 99 parsed, err = ParseFmtTemplate(template) 100 if err == nil || err != errTemplateNotFmt { 101 return 102 } 103 104 // "pure" prefix w/ no ranges 105 return ParsedTemplate{Prefix: template}, nil 106 } 107 108 func (pt *ParsedTemplate) Clone() *ParsedTemplate { 109 if pt == nil { 110 return nil 111 } 112 clone := *pt 113 return &clone 114 } 115 116 func (pt *ParsedTemplate) Count() int64 { 117 count := int64(1) 118 for _, tr := range pt.Ranges { 119 step := (tr.End-tr.Start)/tr.Step + 1 120 count *= step 121 } 122 return count 123 } 124 125 // maxLen specifies maximum objects to be returned 126 func (pt *ParsedTemplate) ToSlice(maxLen ...int) []string { 127 var i, n int 128 if len(maxLen) > 0 && maxLen[0] >= 0 { 129 n = maxLen[0] 130 } else { 131 n = int(pt.Count()) 132 } 133 objs := make([]string, 0, n) 134 pt.InitIter() 135 for objName, hasNext := pt.Next(); hasNext && i < n; objName, hasNext = pt.Next() { 136 objs = append(objs, objName) 137 i++ 138 } 139 return objs 140 } 141 142 func (pt *ParsedTemplate) InitIter() { 143 pt.rangesCount = len(pt.Ranges) 144 pt.at = make([]int64, pt.rangesCount) 145 for i, tr := range pt.Ranges { 146 pt.at[i] = tr.Start 147 } 148 } 149 150 func (pt *ParsedTemplate) Next() (string, bool) { 151 pt.buf.Reset() 152 for i := pt.rangesCount - 1; i >= 0; i-- { 153 if pt.at[i] > pt.Ranges[i].End { 154 if i == 0 { 155 return "", false 156 } 157 pt.at[i] = pt.Ranges[i].Start 158 pt.at[i-1] += pt.Ranges[i-1].Step 159 } 160 } 161 pt.buf.WriteString(pt.Prefix) 162 for i, tr := range pt.Ranges { 163 pt.buf.WriteString(fmt.Sprintf("%0*d%s", tr.DigitCount, pt.at[i], tr.Gap)) 164 } 165 pt.at[pt.rangesCount-1] += pt.Ranges[pt.rangesCount-1].Step 166 return pt.buf.String(), true 167 } 168 169 // 170 // parsing --- parsing --- parsing 171 // 172 173 // template: "prefix-%06d-suffix" 174 // (both prefix and suffix are optional, here and elsewhere) 175 func ParseFmtTemplate(template string) (pt ParsedTemplate, err error) { 176 percent := strings.IndexByte(template, '%') 177 if percent == -1 { 178 err = errTemplateNotFmt 179 return 180 } 181 if idx := strings.IndexByte(template[percent+1:], '%'); idx != -1 { 182 err = errTemplateNotFmt 183 return 184 } 185 186 d := strings.IndexByte(template[percent:], 'd') 187 if d == -1 { 188 err = newErrTemplateInvalid(invalidFmt, template) 189 return 190 } 191 d += percent 192 193 digitCount := 0 194 if d-percent > 1 { 195 s := template[percent+1 : d] 196 if len(s) == 1 { 197 err = newErrTemplateInvalid(invalidFmt, template) 198 return 199 } 200 if s[0] != '0' { 201 err = newErrTemplateInvalid(invalidFmt, template) 202 return 203 } 204 i, err := strconv.ParseInt(s[1:], 10, 64) 205 if err != nil { 206 return pt, newErrTemplateInvalid(invalidFmt, template) 207 } else if i < 0 { 208 return pt, newErrTemplateInvalid(invalidFmt, template) 209 } 210 digitCount = int(i) 211 } 212 213 return ParsedTemplate{ 214 Prefix: template[:percent], 215 Ranges: []TemplateRange{{ 216 Start: 0, 217 End: math.MaxInt64 - 1, 218 Step: 1, 219 DigitCount: digitCount, 220 Gap: template[d+1:], 221 }}, 222 }, nil 223 } 224 225 // examples 226 // - single-range: "prefix{0001..0010}suffix" 227 // - multi-range: "prefix-{00001..00010..2}-gap-{001..100..2}-suffix" 228 // (both prefix and suffix are optional, here and elsewhere) 229 func ParseBashTemplate(template string) (pt ParsedTemplate, err error) { 230 left := strings.IndexByte(template, '{') 231 if left == -1 { 232 err = errTemplateNotBash 233 return 234 } 235 right := strings.LastIndexByte(template, '}') 236 if right == -1 { 237 err = errTemplateNotBash 238 return 239 } 240 if right < left { 241 err = newErrTemplateInvalid(invalidBash, template) 242 return 243 } 244 pt.Prefix = template[:left] 245 246 for { 247 tr := TemplateRange{} 248 249 left := strings.IndexByte(template, '{') 250 if left == -1 { 251 break 252 } 253 254 right := strings.IndexByte(template, '}') 255 if right == -1 { 256 err = newErrTemplateInvalid(invalidBash, template) 257 return 258 } 259 if right < left { 260 err = newErrTemplateInvalid(invalidBash, template) 261 return 262 } 263 inside := template[left+1 : right] 264 265 numbers := strings.Split(inside, "..") 266 if len(numbers) < 2 || len(numbers) > 3 { 267 err = newErrTemplateInvalid(invalidBash, template) 268 return 269 } else if len(numbers) == 2 { // {0001..0999} case 270 if tr.Start, err = strconv.ParseInt(numbers[0], 10, 64); err != nil { 271 return 272 } 273 if tr.End, err = strconv.ParseInt(numbers[1], 10, 64); err != nil { 274 return 275 } 276 tr.Step = 1 277 tr.DigitCount = Min(len(numbers[0]), len(numbers[1])) 278 } else if len(numbers) == 3 { // {0001..0999..2} case 279 if tr.Start, err = strconv.ParseInt(numbers[0], 10, 64); err != nil { 280 return 281 } 282 if tr.End, err = strconv.ParseInt(numbers[1], 10, 64); err != nil { 283 return 284 } 285 if tr.Step, err = strconv.ParseInt(numbers[2], 10, 64); err != nil { 286 return 287 } 288 tr.DigitCount = Min(len(numbers[0]), len(numbers[1])) 289 } 290 if err = validateBoundaries("bash", template, tr.Start, tr.End, tr.Step); err != nil { 291 return 292 } 293 294 // apply gap (either to next range or end of the template) 295 template = template[right+1:] 296 right = strings.Index(template, "{") 297 if right >= 0 { 298 tr.Gap = template[:right] 299 } else { 300 tr.Gap = template 301 } 302 303 pt.Ranges = append(pt.Ranges, tr) 304 } 305 return 306 } 307 308 // e.g.: 309 // - multi range: "prefix-@00001-gap-@100-suffix" 310 // - single range: "prefix@00100suffix" 311 func ParseAtTemplate(template string) (pt ParsedTemplate, err error) { 312 left := strings.IndexByte(template, '@') 313 if left == -1 { 314 err = errTemplateNotAt 315 return 316 } 317 pt.Prefix = template[:left] 318 319 for { 320 tr := TemplateRange{} 321 322 left := strings.IndexByte(template, '@') 323 if left == -1 { 324 break 325 } 326 327 number := "" 328 for left++; len(template) > left && unicode.IsDigit(rune(template[left])); left++ { 329 number += string(template[left]) 330 } 331 332 tr.Start = 0 333 if tr.End, err = strconv.ParseInt(number, 10, 64); err != nil { 334 return 335 } 336 tr.Step = 1 337 tr.DigitCount = len(number) 338 339 if err = validateBoundaries("at", template, tr.Start, tr.End, tr.Step); err != nil { 340 return 341 } 342 343 // apply gap (either to next range or end of the template) 344 template = template[left:] 345 right := strings.IndexByte(template, '@') 346 if right >= 0 { 347 tr.Gap = template[:right] 348 } else { 349 tr.Gap = template 350 } 351 352 pt.Ranges = append(pt.Ranges, tr) 353 } 354 return 355 } 356 357 func validateBoundaries(typ, template string, start, end, step int64) error { 358 if start > end { 359 return newErrTemplateInvalid(startAfterEnd, typ, template) 360 } 361 if start < 0 { 362 return newErrTemplateInvalid(negativeStart, typ, template) 363 } 364 if step <= 0 { 365 return newErrTemplateInvalid(nonPositiveStep, typ, template) 366 } 367 return nil 368 }