这个模块儿讲解 Minio 的配置文件中省略号类型配置项的处理。例如在 Minio 官方文档中出现的命令行示例:minio server http://host{1...32}/export{1...32}。
Regular Expression
Minio 处理省略号配置项时,使用了非常简单的正则表达式,通过 前缀 + 省略号项 + 后缀 的形式进行分级处理。我们先看这个正则表达式及相关的符号定义
var (
// Regex to extract ellipses syntax inputs.
regexpEllipses = regexp.MustCompile(`(.*)({[0-9a-z]*\.\.\.[0-9a-z]*})(.*)`)
// Ellipses constants
openBraces = "{"
closeBraces = "}"
ellipses = "..."
)
从正则的定义中可以清楚的看到,以 {[数字、字母]…[数字、字母]} 为界,将一个目标串分为前缀、后缀及配置项自身,下面的程序来进行简单的验证
package main
import (
"regexp"
"fmt"
)
var (
regexpEllipses = regexp.MustCompile(`(.*)({[0-9a-z]*\.\.\.[0-9a-z]*})(.*)`)
)
func main() {
fmt.Println(regexpEllipses.FindStringSubmatch("http://127.0.0.1{0...10}")[1:])
fmt.Println(regexpEllipses.FindStringSubmatch("http://127.0.0.1{0...10}:{20...40}")[1:])
fmt.Println(regexpEllipses.FindStringSubmatch("http://127.0.0.1{0...10}{1..2..}")[1:])
}
// [http://127.0.0.1 {0...10} ]
// [http://127.0.0.1{0...10}: {20...40} ]
// [http://127.0.0.1 {0...10} {1..2..}]
对于第一个示例,只有 {0…10} 符合,因此最终结果没有后缀;第二个例子中,有两项符合条件,但正则优先匹配更长的项,因此依然没有后缀;第三个例子中,第二个大括号内省略号中点的数量为 2,因此不能匹配,便被作为后缀存在。
Find Ellipses Pattern
Pattern
在配置字符串中,要发现包含的省略号配置项,可通过 FindEllipsesPatterns 方法,这个方法返回 ArgPattern 类型实例
func FindEllipsesPatterns(arg string) (ArgPattern, error)
ArgPattern 相关定义如下
type ArgPattern []Pattern
type Pattern struct {
Prefix string
Suffix string
Seq []string
}
在 Pattern 中,Prefix、Suffix 都很容易理解,问题是 Seq 为什么是切片,这个问题需要在后续的代码中找到答案。以及 Pattern 如何处理多个省略号配置,也需要在后续代码中找办法。
FindEllipsesPatterns
func FindEllipsesPatterns(arg string) (ArgPattern, error) {
var patterns []Pattern
parts := regexpEllipses.FindStringSubmatch(arg)
if len(parts) == 0 {
// We throw an error if arg doesn't have any recognizable ellipses pattern.
return nil, ErrInvalidEllipsesFormatFn(arg)
} // [1]
parts = parts[1:]
patternFound := regexpEllipses.MatchString(parts[0])
for patternFound {
seq, err := parseEllipsesRange(parts[1]) // [2]
if err != nil {
return patterns, err
}
patterns = append(patterns, Pattern{
Prefix: "",
Suffix: parts[2],
Seq: seq,
}) // [3]
parts = regexpEllipses.FindStringSubmatch(parts[0])
if len(parts) > 0 {
parts = parts[1:]
patternFound = HasEllipses(parts[0])
continue
} // [4]
break
}
if len(parts) > 0 {
seq, err := parseEllipsesRange(parts[1])
if err != nil {
return patterns, err
}
patterns = append(patterns, Pattern{
Prefix: parts[0],
Suffix: parts[2],
Seq: seq,
})
} // [5]
// Check if any of the prefix or suffixes now have flower braces
// left over, in such a case we generally think that there is
// perhaps a typo in users input and error out accordingly.
for _, pattern := range patterns {
if strings.Count(pattern.Prefix, openBraces) > 0 || strings.Count(pattern.Prefix, closeBraces) > 0 {
return nil, ErrInvalidEllipsesFormatFn(arg)
}
if strings.Count(pattern.Suffix, openBraces) > 0 || strings.Count(pattern.Suffix, closeBraces) > 0 {
return nil, ErrInvalidEllipsesFormatFn(arg)
}
} // [6]
return patterns, nil
}
[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] 这样的顺序串,下面我们结合代码来进行分析,函数的声明如下所示
func parseEllipsesRange(pattern string) (seq []string, err error)
首先进行验证输入的字符串是否包含了大括号
if !strings.Contains(pattern, openBraces) {
return nil, errors.New("Invalid argument")
}
if !strings.Contains(pattern, closeBraces) {
return nil, errors.New("Invalid argument")
}
清除大括号前后无关内容,并使用 … 分割字符串
pattern = strings.TrimPrefix(pattern, openBraces)
pattern = strings.TrimSuffix(pattern, closeBraces)
ellipsesRange := strings.Split(pattern, ellipses)
if len(ellipsesRange) != 2 {
return nil, errors.New("Invalid argument")
}
获取数字序列的开始值与结束值,及是否 16 进制表示,从代码中可以看到,数字表示形式优先使用 10 进制,如果转换失败才考虑 16 进制
var hexadecimal bool
var start, end uint64
if start, err = strconv.ParseUint(ellipsesRange[0], 10, 64); err != nil {
// Look for hexadecimal conversions if any.
start, err = strconv.ParseUint(ellipsesRange[0], 16, 64)
if err != nil {
return nil, err
}
hexadecimal = true
}
if end, err = strconv.ParseUint(ellipsesRange[1], 10, 64); err != nil {
// Look for hexadecimal conversions if any.
end, err = strconv.ParseUint(ellipsesRange[1], 16, 64)
if err != nil {
return nil, err
}
hexadecimal = true
}
检查开始值与结束值是否合理
if start > end {
return nil, fmt.Errorf("Incorrect range start %d cannot be bigger than end %d", start, end)
}
从开始值至结束值,生成字符串序列并返回,唯一需要注意的是关于补 0 的情况,在开始值的字符串形式中,以 0 开始,且不为 0 或者结束值的字符串形式中以 0 起始时,输出的字符串序列才会补 0,且每一个序列长度都与结束值字符串长度相同。
for i := start; i <= end; i++ {
if strings.HasPrefix(ellipsesRange[0], "0") && len(ellipsesRange[0]) > 1 || strings.HasPrefix(ellipsesRange[1], "0") {
if hexadecimal {
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", len(ellipsesRange[1])), i))
} else {
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dd", len(ellipsesRange[1])), i))
}
} else {
if hexadecimal {
seq = append(seq, fmt.Sprintf("%x", i))
} else {
seq = append(seq, fmt.Sprintf("%d", i))
}
}
}
return seq, nil
Expand
在完成配置项模式识别后,形成了一个 Pattern 的切片,后面就需要根据 Pattern 的组合,形成真正的配置项,这个过程叫做 Expand。Expand 需要 ArgPattern 与 Pattern 配合完成,接下来我们分别来进行的分析。
Pattern
Pattern 的 Expand 方法相对简单,根据 Prefix、Suffix 是否为空的组合输出最终的配置字符串即可。一个 Pattern 的 Expand 方法会返回一个字符串切片。
func (p Pattern) Expand() []string {
var labels []string
for i := range p.Seq {
switch {
case p.Prefix != "" && p.Suffix == "":
labels = append(labels, fmt.Sprintf("%s%s", p.Prefix, p.Seq[i]))
case p.Suffix != "" && p.Prefix == "":
labels = append(labels, fmt.Sprintf("%s%s", p.Seq[i], p.Suffix))
case p.Suffix == "" && p.Prefix == "":
labels = append(labels, p.Seq[i])
default:
labels = append(labels, fmt.Sprintf("%s%s%s", p.Prefix, p.Seq[i], p.Suffix))
}
}
return labels
}
ArgPattern
ArgPattern 的 Expand 方法相对复杂一些,由于 ArgPattern 中包含了一组相关联的 Pattern,因此需要按从首到尾顺序将每个 Pattern 进行展开操作,然后再按照从尾到首的顺序进行合并,代码如下
func (a ArgPattern) Expand() [][]string {
labels := make([][]string, len(a))
for i := range labels {
labels[i] = a[i].Expand()
}
return argExpander(labels)
}
argExpander 进行自尾而首的合并,使用了递归实现,代码如下
func argExpander(labels [][]string) (out [][]string) {
if len(labels) == 1 {
for _, v := range labels[0] {
out = append(out, []string{v})
}
return out
}
for _, lbl := range labels[0] {
rs := argExpander(labels[1:])
for _, rlbls := range rs {
r := append(rlbls, []string{lbl}...)
out = append(out, r)
}
}
return out
}
大致的执行过程如下图所示