这个模块儿讲解 Minio 的配置文件中省略号类型配置项的处理。例如在 Minio 官方文档中出现的命令行示例:minio server http://host{1...32}/export{1...32}。

Regular Expression

Minio 处理省略号配置项时,使用了非常简单的正则表达式,通过 前缀 + 省略号项 + 后缀 的形式进行分级处理。我们先看这个正则表达式及相关的符号定义

  1. var (
  2. // Regex to extract ellipses syntax inputs.
  3. regexpEllipses = regexp.MustCompile(`(.*)({[0-9a-z]*\.\.\.[0-9a-z]*})(.*)`)
  4. // Ellipses constants
  5. openBraces = "{"
  6. closeBraces = "}"
  7. ellipses = "..."
  8. )

从正则的定义中可以清楚的看到,以 {[数字、字母]…[数字、字母]} 为界,将一个目标串分为前缀、后缀及配置项自身,下面的程序来进行简单的验证

  1. package main
  2. import (
  3. "regexp"
  4. "fmt"
  5. )
  6. var (
  7. regexpEllipses = regexp.MustCompile(`(.*)({[0-9a-z]*\.\.\.[0-9a-z]*})(.*)`)
  8. )
  9. func main() {
  10. fmt.Println(regexpEllipses.FindStringSubmatch("http://127.0.0.1{0...10}")[1:])
  11. fmt.Println(regexpEllipses.FindStringSubmatch("http://127.0.0.1{0...10}:{20...40}")[1:])
  12. fmt.Println(regexpEllipses.FindStringSubmatch("http://127.0.0.1{0...10}{1..2..}")[1:])
  13. }
  14. // [http://127.0.0.1 {0...10} ]
  15. // [http://127.0.0.1{0...10}: {20...40} ]
  16. // [http://127.0.0.1 {0...10} {1..2..}]

对于第一个示例,只有 {0…10} 符合,因此最终结果没有后缀;第二个例子中,有两项符合条件,但正则优先匹配更长的项,因此依然没有后缀;第三个例子中,第二个大括号内省略号中点的数量为 2,因此不能匹配,便被作为后缀存在。

Find Ellipses Pattern

Pattern

在配置字符串中,要发现包含的省略号配置项,可通过 FindEllipsesPatterns 方法,这个方法返回 ArgPattern 类型实例

  1. func FindEllipsesPatterns(arg string) (ArgPattern, error)

ArgPattern 相关定义如下

  1. type ArgPattern []Pattern
  2. type Pattern struct {
  3. Prefix string
  4. Suffix string
  5. Seq []string
  6. }

在 Pattern 中,Prefix、Suffix 都很容易理解,问题是 Seq 为什么是切片,这个问题需要在后续的代码中找到答案。以及 Pattern 如何处理多个省略号配置,也需要在后续代码中找办法。

FindEllipsesPatterns

  1. func FindEllipsesPatterns(arg string) (ArgPattern, error) {
  2. var patterns []Pattern
  3. parts := regexpEllipses.FindStringSubmatch(arg)
  4. if len(parts) == 0 {
  5. // We throw an error if arg doesn't have any recognizable ellipses pattern.
  6. return nil, ErrInvalidEllipsesFormatFn(arg)
  7. } // [1]
  8. parts = parts[1:]
  9. patternFound := regexpEllipses.MatchString(parts[0])
  10. for patternFound {
  11. seq, err := parseEllipsesRange(parts[1]) // [2]
  12. if err != nil {
  13. return patterns, err
  14. }
  15. patterns = append(patterns, Pattern{
  16. Prefix: "",
  17. Suffix: parts[2],
  18. Seq: seq,
  19. }) // [3]
  20. parts = regexpEllipses.FindStringSubmatch(parts[0])
  21. if len(parts) > 0 {
  22. parts = parts[1:]
  23. patternFound = HasEllipses(parts[0])
  24. continue
  25. } // [4]
  26. break
  27. }
  28. if len(parts) > 0 {
  29. seq, err := parseEllipsesRange(parts[1])
  30. if err != nil {
  31. return patterns, err
  32. }
  33. patterns = append(patterns, Pattern{
  34. Prefix: parts[0],
  35. Suffix: parts[2],
  36. Seq: seq,
  37. })
  38. } // [5]
  39. // Check if any of the prefix or suffixes now have flower braces
  40. // left over, in such a case we generally think that there is
  41. // perhaps a typo in users input and error out accordingly.
  42. for _, pattern := range patterns {
  43. if strings.Count(pattern.Prefix, openBraces) > 0 || strings.Count(pattern.Prefix, closeBraces) > 0 {
  44. return nil, ErrInvalidEllipsesFormatFn(arg)
  45. }
  46. if strings.Count(pattern.Suffix, openBraces) > 0 || strings.Count(pattern.Suffix, closeBraces) > 0 {
  47. return nil, ErrInvalidEllipsesFormatFn(arg)
  48. }
  49. } // [6]
  50. return patterns, nil
  51. }

[1] L3 ~ L7:正则表达式解析符合条件的字串,如果匹配失败,直接退出;
[2] L12: 在解析出的省略号配置中生产 Seq;
[3] L16 ~ L20: 创建 Pattern 实例,注意此处 Prefix 为空,Suffix 为切片最后一个元素;
[4] L21 ~ L26: 递归处理 Prefix 对应的切片位置(索引 0 处),如果不包含省略号类型配置,退出;
[5] L30 ~ L41: 处理最后一个满足条件的模式,找到真正的 Prefix、Suffix;
[6] L46 ~ L53: 检查全部模式,确保全部模式的 Prefix、Suffix 中均不包含未处理的省略号配置。

parseEllipsesRange

parseEllipsesRange 将形如 {1…10} 的配置转变为 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 这样的顺序串,下面我们结合代码来进行分析,函数的声明如下所示

  1. func parseEllipsesRange(pattern string) (seq []string, err error)

首先进行验证输入的字符串是否包含了大括号

  1. if !strings.Contains(pattern, openBraces) {
  2. return nil, errors.New("Invalid argument")
  3. }
  4. if !strings.Contains(pattern, closeBraces) {
  5. return nil, errors.New("Invalid argument")
  6. }

清除大括号前后无关内容,并使用 … 分割字符串

  1. pattern = strings.TrimPrefix(pattern, openBraces)
  2. pattern = strings.TrimSuffix(pattern, closeBraces)
  3. ellipsesRange := strings.Split(pattern, ellipses)
  4. if len(ellipsesRange) != 2 {
  5. return nil, errors.New("Invalid argument")
  6. }

获取数字序列的开始值与结束值,及是否 16 进制表示,从代码中可以看到,数字表示形式优先使用 10 进制,如果转换失败才考虑 16 进制

  1. var hexadecimal bool
  2. var start, end uint64
  3. if start, err = strconv.ParseUint(ellipsesRange[0], 10, 64); err != nil {
  4. // Look for hexadecimal conversions if any.
  5. start, err = strconv.ParseUint(ellipsesRange[0], 16, 64)
  6. if err != nil {
  7. return nil, err
  8. }
  9. hexadecimal = true
  10. }
  11. if end, err = strconv.ParseUint(ellipsesRange[1], 10, 64); err != nil {
  12. // Look for hexadecimal conversions if any.
  13. end, err = strconv.ParseUint(ellipsesRange[1], 16, 64)
  14. if err != nil {
  15. return nil, err
  16. }
  17. hexadecimal = true
  18. }

检查开始值与结束值是否合理

  1. if start > end {
  2. return nil, fmt.Errorf("Incorrect range start %d cannot be bigger than end %d", start, end)
  3. }

从开始值至结束值,生成字符串序列并返回,唯一需要注意的是关于补 0 的情况,在开始值的字符串形式中,以 0 开始,且不为 0 或者结束值的字符串形式中以 0 起始时,输出的字符串序列才会补 0,且每一个序列长度都与结束值字符串长度相同。

  1. for i := start; i <= end; i++ {
  2. if strings.HasPrefix(ellipsesRange[0], "0") && len(ellipsesRange[0]) > 1 || strings.HasPrefix(ellipsesRange[1], "0") {
  3. if hexadecimal {
  4. seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", len(ellipsesRange[1])), i))
  5. } else {
  6. seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dd", len(ellipsesRange[1])), i))
  7. }
  8. } else {
  9. if hexadecimal {
  10. seq = append(seq, fmt.Sprintf("%x", i))
  11. } else {
  12. seq = append(seq, fmt.Sprintf("%d", i))
  13. }
  14. }
  15. }
  16. return seq, nil

Expand

在完成配置项模式识别后,形成了一个 Pattern 的切片,后面就需要根据 Pattern 的组合,形成真正的配置项,这个过程叫做 Expand。Expand 需要 ArgPattern 与 Pattern 配合完成,接下来我们分别来进行的分析。

Pattern

Pattern 的 Expand 方法相对简单,根据 Prefix、Suffix 是否为空的组合输出最终的配置字符串即可。一个 Pattern 的 Expand 方法会返回一个字符串切片。

  1. func (p Pattern) Expand() []string {
  2. var labels []string
  3. for i := range p.Seq {
  4. switch {
  5. case p.Prefix != "" && p.Suffix == "":
  6. labels = append(labels, fmt.Sprintf("%s%s", p.Prefix, p.Seq[i]))
  7. case p.Suffix != "" && p.Prefix == "":
  8. labels = append(labels, fmt.Sprintf("%s%s", p.Seq[i], p.Suffix))
  9. case p.Suffix == "" && p.Prefix == "":
  10. labels = append(labels, p.Seq[i])
  11. default:
  12. labels = append(labels, fmt.Sprintf("%s%s%s", p.Prefix, p.Seq[i], p.Suffix))
  13. }
  14. }
  15. return labels
  16. }

ArgPattern

ArgPattern 的 Expand 方法相对复杂一些,由于 ArgPattern 中包含了一组相关联的 Pattern,因此需要按从首到尾顺序将每个 Pattern 进行展开操作,然后再按照从尾到首的顺序进行合并,代码如下

  1. func (a ArgPattern) Expand() [][]string {
  2. labels := make([][]string, len(a))
  3. for i := range labels {
  4. labels[i] = a[i].Expand()
  5. }
  6. return argExpander(labels)
  7. }

argExpander 进行自尾而首的合并,使用了递归实现,代码如下

  1. func argExpander(labels [][]string) (out [][]string) {
  2. if len(labels) == 1 {
  3. for _, v := range labels[0] {
  4. out = append(out, []string{v})
  5. }
  6. return out
  7. }
  8. for _, lbl := range labels[0] {
  9. rs := argExpander(labels[1:])
  10. for _, rlbls := range rs {
  11. r := append(rlbls, []string{lbl}...)
  12. out = append(out, r)
  13. }
  14. }
  15. return out
  16. }

大致的执行过程如下图所示
cli-argpattern-expand.svg