正则表达式更新多行字符串并保留缩进

regex to update multi-lined string and preserve indentation

我有数百个 YAML 文件需要不时更新。

更新前:

sss:
  - ccc:
      brr: 'mmm'
      jdk: 'openjdk8'
  - bbb:
      brr: 'rel/bbb'
      jdk: 'openjdk8'
  - aaa:
      brr: 'rel/aaa'
      jdk: 'openjdk7'

更新后:

sss:
  - ddd: 
      brr: 'mmm'
      jdk: 'openjdk8'
  - ccc:
      brr: 'rel/ccc'
      jdk: 'openjdk8'
  - bbb:
      brr: 'rel/bbb'
      jdk: 'openjdk8'
  - aaa:
      brr: 'rel/aaa'
      jdk: 'openjdk7'
  1. 对于任何文件中出现的以下模式:
sss:
  - ccc:
      brr: 'mmm'
  1. 替换并修改上述模式,将 'mmm' 替换为 'rel/ccc'
  - ccc:
      brr: 'rel/ccc'
  1. 以以下格式创建新的子字符串(多行):
  - new: 
      brr: 'new-mmm'
      jdk: 'openjdk8'
  1. 合并 2. 和 3. 并将原始文件替换为:
sss:
  - new: 
      brr: 'mmm'
      jdk: 'openjdk8'
  - ccc:
      brr: 'rel/ccc'
      jdk: 'openjdk8'

例如,我们需要更新上述文件,使其看起来像在每一行中保留白色 spaces/tabs,因为格式化对于 YAML 很重要。

我已经用 PyYAML 尝试过,但由于语法复杂而无法正常工作。这可以通过使用 awk, sed 捕获空格来完成吗?

试试这个 awk 程序:

/sss:/ { sss = 1; }
/- ccc:/ { ccc = 1; ind = substr([=10=], 1, index([=10=], "-")-1); next; } # don't print
 == "brr:" &&  == "'mmm'" {
    if (sss && ccc) {
        print ind "- ddd:";
        print ind "    brr: 'mmm'";
        print ind "    jdk: 'openjdk8'";
        print ind "- ccc:";
        print ind "    brr: 'rel/ccc'";
        sss = 0; ccc = 0;
    }
    next;
}
{ print }

第一条规则用于标记进入sss块,第二条用于标记ccc块,另外记录缩进深度。第三条规则添加新的和修改的数据,根据记录的深度缩进,然后退出 sssccc 块。最终规则打印刚刚读取的行。第二条和第三条规则中的 next 语句阻止应用以下所有规则。

解析结构化数据,无论是 YAML、HTML、XML 还是 CSV,仅使用正则表达式仅适用于一小部分可能情况。使用 YAML 多行标量,以通用方式处理流式和块式等几乎是不可能的。如果不是这种情况,那么早就有人用 awk 编写了完整的 YAML 解析器。 (awk 没有错,只是不是处理 YAML 的正确工具。

这并不意味着您不能使用正则表达式来查找特定元素,您只需要做一些准备:

import sys
import re
import ruamel.yaml

yaml_str = """\
sss:
  - ccc:
      brr: 'mmm'
      jdk: 'openjdk8'
  - bbb:
      brr: 'rel/bbb'
      jdk: 'openjdk8'
  - aaa:
      brr: 'rel/aaa'
      jdk: 'openjdk7'
"""


class Paths:
    def __init__(self, data, sep=':'):
        self._sep = sep
        self._data = data

    def walk(self, data=None, prefix=None):
        if data is None:
            data = self._data
        if prefix is None:
            prefix = []
        if isinstance(data, dict):
            for idx, k in enumerate(data):
                path_list = prefix + [k]
                yield self._sep.join([str(q) for q in path_list]), path_list, idx, data[k]
                for x in self.walk(data[k], path_list):
                    yield x
        elif isinstance(data, list):
            for idx, k in enumerate(data):
                path_list = prefix + [idx]
                yield self._sep.join([str(q) for q in path_list]), path_list, idx, k
                for x in self.walk(k, path_list):
                    yield x

    def set(self, pl, val):
        pl = pl[:]
        d = self._data
        while(len(pl) > 1):
            d = d[pl.pop(0)]
        d[pl[0]] = val

    def insert_in_list(self, pl, idx, val):
        pl = pl[:]
        d = self._data
        while(len(pl) > 1):
            d = d[pl.pop(0)]
        d.insert(idx, val)


data = ruamel.yaml.round_trip_load(yaml_str, preserve_quotes=True)
paths = Paths(data)
pattern = re.compile('sss:.*:c.*:brr$')
# if you are going to insert/delete use list(paths.walk())
for p, pl, idx, val in list(paths.walk()):
    print('path', p)
    if not pattern.match(p):
        continue
    paths.set(pl, ruamel.yaml.scalarstring.SingleQuotedScalarString('rel/ccc'))
    paths.insert_in_list(pl[:-2], idx, {'new': {
        'brr': ruamel.yaml.scalarstring.SingleQuotedScalarString('mmm'),
        'jdk': ruamel.yaml.scalarstring.SingleQuotedScalarString('openjdk8')
        }})

print('----------')

ruamel.yaml.round_trip_dump(data, sys.stdout)

输出为:

path sss
path sss:0
path sss:0:ccc
path sss:0:ccc:brr
path sss:0:ccc:jdk
path sss:1
path sss:1:bbb
path sss:1:bbb:brr
path sss:1:bbb:jdk
path sss:2
path sss:2:aaa
path sss:2:aaa:brr
path sss:2:aaa:jdk
----------
sss:
- new:
    brr: 'mmm'
    jdk: 'openjdk8'
- ccc:
    brr: 'rel/ccc'
    jdk: 'openjdk8'
- bbb:
    brr: 'rel/bbb'
    jdk: 'openjdk8'
- aaa:
    brr: 'rel/aaa'
    jdk: 'openjdk7'
  1. 打印 "paths" 不是必需的,但在这里可以更好地了解发生了什么。

  2. SingleQuotedScalarString 是获取 YAML 输出中字符串标量周围多余引号所必需的

  3. 字典子类,YAML 映射由 ruamel.yaml 加载,支持 Python 2.7 和 Python 3.5 及更高版本的 .insert(index, key, val),所以你也可以在映射的特定位置插入。