2017-05-07 87 views
1

最近我試圖用ruamel.yaml來管理我的docker-compose服務配置(即docker-compose.yml)。如何使用ruamel.yaml註釋掉YAML部分?

我需要註釋掉&需要時取消註釋服務塊。假設我有以下文件:

version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    srv2: 
     image: alpine 
     container_name: srv2 
     volumes_from: 
      - some-volume 
volumes: 
    some-volume: 

有一些解決方法,以註釋掉SRV2塊?像下面的輸出:

version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    #srv2: 
    # image: alpine 
    # container_name: srv2 
    # volumes_from: 
    #  - some-volume 
volumes: 
    some-volume: 

此外,有一種方法來去掉這一塊?(假設我已經持有原來srv2塊,我只需要一種方法來刪除這些註釋行)

回答

1

如果srv2是一個關鍵是爲所有在你的YAML映射的唯一,那麼「容易」的方式要循環使用直線,測試去除線條的版本是否以srv2:開頭,請注意前導空格的數量並註釋掉該行,並按照直到您注意到具有等於或少於前導空格的行。這樣做的好處除了簡單和快捷以外,它可以處理不規則的縮進(例如:在srv1之前的4個位置和在some-volume之前的6個位置)。

在使用ruamel.yaml時也可以這樣做,但不太簡單。您必須知道,當round_trip_loading時,ruamel.yaml通常會對已處理的最後一個結構(映射/序列)附加註釋,並且由於該註釋的結果srv1在您的示例中完全不同於srv2(即第一個鍵值對,如果註釋掉,則不同於所有其他鍵值對)。

如果您正常化的預期輸出到四個位置縮進和srv1之前添加註釋供分析之用,加載,你可以搜索在評論結束:

from ruamel.yaml.util import load_yaml_guess_indent 

yaml_str = """\ 
version: '2' 
services: 
    #a 
    #b 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    #srv2: 
    # image: alpine 
    # container_name: srv2 
    # volumes_from: 
    #  - some-volume 
volumes: 
    some-volume: 
""" 

data, indent, block_seq_indent = load_yaml_guess_indent(yaml_str) 
print('indent', indent, block_seq_indent) 

c0 = data['services'].ca 
print('c0:', c0) 
c0_0 = c0.comment[1][0] 
print('c0_0:', repr(c0_0.value), c0_0.start_mark.column) 

c1 = data['services']['srv1']['volumes'].ca 
print('c1:', c1) 
c1_0 = c1.end[0] 
print('c1_0:', repr(c1_0.value), c1_0.start_mark.column) 

它打印:

indent 4 2 
c0: Comment(comment=[None, [CommentToken(), CommentToken()]], 
    items={}) 
c0_0: '#a\n' 4 
c1: Comment(comment=[None, None], 
    items={}, 
    end=[CommentToken(), CommentToken(), CommentToken(), CommentToken(), CommentToken()]) 
c1_0: '#srv2:\n' 4 

所以,你「只」,如果有您註釋掉第一個鍵值對創建第一個類型的註釋(c0),你必須創建其他(c1)如果您註釋掉任何行吟詩人r鍵值對。 startmarkStreamMark()(來自ruamel/yaml/error.py),創建註釋時該實例的唯一重要屬性爲column

幸運的是,上面顯示的過程稍微簡單一些,因爲沒有必要將註釋附加到值volumes的「結尾」,將它們附加到值爲srv1的末尾具有相同的效果。

在下面的comment_block需要一個鍵列表,它是要註釋的元素的路徑。

import sys 
from copy import deepcopy 
from ruamel.yaml import round_trip_dump 
from ruamel.yaml.util import load_yaml_guess_indent 
from ruamel.yaml.error import StreamMark 
from ruamel.yaml.tokens import CommentToken 


yaml_str = """\ 
version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
volumes: 
    some-volume: 
""" 


def comment_block(d, key_index_list, ind, bsi): 
    parent = d 
    for ki in key_index_list[:-1]: 
     parent = parent[ki] 
    # don't just pop the value for key_index_list[-1] that way you lose comments 
    # in the original YAML, instead deepcopy and delete what is not needed 
    data = deepcopy(parent) 
    keys = list(data.keys()) 
    found = False 
    previous_key = None 
    for key in keys: 
     if key != key_index_list[-1]: 
      if not found: 
       previous_key = key 
      del data[key] 
     else: 
      found = True 
    # now delete the key and its value 
    del parent[key_index_list[-1]] 
    if previous_key is None: 
     if parent.ca.comment is None: 
      parent.ca.comment = [None, []] 
     comment_list = parent.ca.comment[1] 
    else: 
     comment_list = parent[previous_key].ca.end = [] 
     parent[previous_key].ca.comment = [None, None] 
    # startmark can be the same for all lines, only column attribute is used 
    start_mark = StreamMark(None, None, None, ind * (len(key_index_list) - 1)) 
    for line in round_trip_dump(data, indent=ind, block_seq_indent=bsi).splitlines(True): 
     comment_list.append(CommentToken('#' + line, start_mark, None)) 

for srv in ['srv1', 'srv2']: 
    data, indent, block_seq_indent = load_yaml_guess_indent(yaml_str) 
    comment_block(data, ['services', srv], ind=indent, bsi=block_seq_indent) 
    round_trip_dump(data, sys.stdout, 
        indent=indent, block_seq_indent=block_seq_indent, 
        explicit_end=True, 
    ) 

它打印:

version: '2' 
services: 
    #srv1: 
    # image: alpine 
    # container_name: srv1 
    # volumes: 
    #  - some-volume:/some/path 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
volumes: 
    some-volume: 
... 
version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    #srv2: 
    # image: alpine 
    # container_name: srv2  # second container 
    # volumes_from: 
    #  - some-volume 
volumes: 
    some-volume: 
... 

(該explicit_end=True是沒有必要的,這裏用來獲取兩個YAML之間的一些界限自動轉儲)。

以這種方式刪除評論也可以完成。遞歸搜索註釋屬性(.ca)以查找註釋掉的候選人(可能會提供關於從何處開始的提示)。從註釋中去除前導#並連接,然後round_trip_load。根據註釋列,您可以確定在哪裏附加未註釋的鍵值對。

+0

我的樣本輸出是嚴格通過4個空格縮進,它的怪異,爲什麼它打印6您的瀏覽器中有空格。 – cherrot

+0

@cherrot在'some-volume:'之前不是,在它們之前有6個短劃線偏移4的縮進(即塊順序縮進)。這當然是你如何計算的,但是像'-a'這樣的序列元素被計數爲縮進2,偏移量爲0.這就是'某個音量'的's'比'v'的'v'卷「,這是計爲6 indents – Anthon

+0

@cherrot這不是我想出的,這是PyYAML只有一個」縮進「控制的映射和序列中的短劃線不計數的結果。我曾經考慮過把它分成兩個參數ruamel.yaml,但遇到了很多問題。添加'block-sequence-indent'是目前我能做的最好的。 – Anthon

1

添加uncomment_block功能由@安通的回答啓發,以及一些增強功能comment_block

from copy import deepcopy 
from ruamel.yaml import round_trip_dump, round_trip_load 
from ruamel.yaml.error import StreamMark 
from ruamel.yaml.tokens import CommentToken 


def comment_block(root, key_hierarchy_list, indent, seq_indent): 
    found = False 
    comment_key = key_hierarchy_list[-1] 
    parent = root 
    for ki in key_hierarchy_list[:-1]: 
     parent = parent[ki] 
    # don't just pop the value for key_hierarchy_list[-1] that way you lose comments 
    # in the original YAML, instead deepcopy and delete what is not needed 
    block_2b_commented = deepcopy(parent) 
    previous_key = None 
    for key in parent.keys(): 
     if key == comment_key: 
      found = True 
     else: 
      if not found: 
       previous_key = key 
      del block_2b_commented[key] 

    # now delete the key and its value, but preserve its preceding comments 
    preceding_comments = parent.ca.items.get(comment_key, [None, None, None, None])[1] 
    del parent[comment_key] 

    if previous_key is None: 
     if parent.ca.comment is None: 
      parent.ca.comment = [None, []] 
     comment_list = parent.ca.comment[1] 
    else: 
     comment_list = parent[previous_key].ca.end = [] 
     parent[previous_key].ca.comment = [None, None] 

    if preceding_comments is not None: 
     comment_list.extend(preceding_comments) 

    # startmark can be the same for all lines, only column attribute is used 
    start_mark = StreamMark(None, None, None, indent * (len(key_hierarchy_list) - 1)) 
    skip = True 
    for line in round_trip_dump(block_2b_commented, indent=indent, block_seq_indent=seq_indent).splitlines(True): 
     if skip: 
      if not line.startswith(comment_key + ':'): 
       continue 
      skip = False 
     comment_list.append(CommentToken('#' + line, start_mark, None)) 

    return False 


def uncomment_block(root, key_hierarchy_list, indent, seq_indent): 
    ''' 
    FIXME: comments may be attached to the parent's neighbour 
    in document like the following. (srv2 block is attached by volumes, not servies, not srv1). 
    version: '2' 
     services: 
      srv1: foobar 
      #srv2: 
      # image: alpine 
      # container_name: srv2 
      # volumes_from: 
      #  - some-volume 
     volumes: 
      some-volume: 
    ''' 
    found = False 
    parent = root 
    commented_key = key_hierarchy_list[-1] 
    comment_indent = indent * (len(key_hierarchy_list) - 1) 
    for ki in key_hierarchy_list[:-1]: 
     parent = parent[ki] 

    if parent.ca.comment is not None: 
     comment_list = parent.ca.comment[1] 
     found, start, stop = _locate_comment_boundary(comment_list, commented_key, comment_indent) 

    if not found: 
     for key in parent.keys(): 
      bro = parent[key] 
      while hasattr(bro, 'keys') and bro.keys(): 
       bro = bro[bro.keys()[-1]] 

      if not hasattr(bro, 'ca'): 
       continue 

      comment_list = bro.ca.end 
      found, start, stop = _locate_comment_boundary(comment_list, commented_key, comment_indent) 

    if found: 
     block_str = u'' 
     commented = comment_list[start:stop] 
     for ctoken in commented: 
      block_str += ctoken.value.replace('#', '', 1) 
     del(comment_list[start:stop]) 

     block = round_trip_load(block_str) 
     parent.update(block) 
    return found 


def _locate_comment_boundary(comment_list, commented_key, comment_indent): 
    found = False 
    start_idx = 0 
    stop_idx = len(comment_list) 
    for idx, ctoken in enumerate(comment_list): 
     if not found: 
      if ctoken.start_mark.column == comment_indent\ 
        and ctoken.value.replace('#', '', 1).startswith(commented_key): 
       found = True 
       start_idx = idx 
     elif ctoken.start_mark.column != comment_indent: 
      stop_idx = idx 
      break 
    return found, start_idx, stop_idx 


if __name__ == "__main__": 
    import sys 
    from ruamel.yaml.util import load_yaml_guess_indent 

    yaml_str = """\ 
version: '2' 
services: 
    # 1 indent after services 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
volumes: 
    some-volume: 
""" 

    for srv in ['srv1', 'srv2']: 
     # Comment a service block 
     yml, indent, block_seq_indent = load_yaml_guess_indent(yaml_str) 
     comment_block(yml, ['services', srv], indent=indent, seq_indent=block_seq_indent) 
     commented = round_trip_dump(
      yml, indent=indent, block_seq_indent=block_seq_indent, explicit_end=True, 
     ) 
     print(commented) 

     # Now uncomment it 
     yml, indent, block_seq_indent = load_yaml_guess_indent(commented) 
     uncomment_block(yml, ['services', srv], indent=indent, seq_indent=block_seq_indent) 

     round_trip_dump(
      yml, sys.stdout, indent=indent, block_seq_indent=block_seq_indent, explicit_end=True, 
     ) 

輸出:

version: '2' 
services: 
    # 1 indent after services 
    #srv1: 
    # image: alpine 
    # container_name: srv1 
    # volumes: 
    #  - some-volume 
    #  # some comments 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
volumes: 
    some-volume: 
... 

version: '2' 
services: 
    # 1 indent after services 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
volumes: 
    some-volume: 
... 
version: '2' 
services: 
    # 1 indent after services 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
    #srv2: 
    # image: alpine 
    # container_name: srv2  # second container 
    # volumes_from: 
    #  - some-volume 
    #  # 2 indent after srv2 volume 
    ## 0 indent before volumes 
volumes: 
    some-volume: 
... 

version: '2' 
services: 
    # 1 indent after services 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
volumes: 
    some-volume: 
...