这个模块儿讲解 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 constantsopenBraces = "{"closeBraces = "}"ellipses = "...")
从正则的定义中可以清楚的看到,以 {[数字、字母]…[数字、字母]} 为界,将一个目标串分为前缀、后缀及配置项自身,下面的程序来进行简单的验证
package mainimport ("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 []Patterntype Pattern struct {Prefix stringSuffix stringSeq []string}
在 Pattern 中,Prefix、Suffix 都很容易理解,问题是 Seq 为什么是切片,这个问题需要在后续的代码中找到答案。以及 Pattern 如何处理多个省略号配置,也需要在后续代码中找办法。
FindEllipsesPatterns
func FindEllipsesPatterns(arg string) (ArgPattern, error) {var patterns []Patternparts := 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 boolvar start, end uint64if 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 []stringfor 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}
大致的执行过程如下图所示
