Search code examples
yamlyq

Insert a key after another key in a YAML file


Sample YAML file:

key1: value1
key2: value2
key3: value3

How can I add the keys key2a and key2b after key2 using Mike Farah’s yq? Although the order of keys is technical and of minor importance, YAML files frequently serve as configuration files. I want to maintain a specific order and grouping for human editors.

Desired result:

key1: value1
key2: value2
key2a: value2a
key2b: value2b
key3: value3

A similar question was asked some time ago but referred to the Python version of yq.


Solution

  • Using mikefarah/yq, you could traverse and rebuild the structure using ireduce, while inserting more items if conditions match.

    Note #1: This approach uses the alternative operator // to compensate for an if conditional which is still missing in yq's most recent release v.4.31.1 (Feb 20, 2023).

    (.[] | [key, .]) as $i ireduce({}; .[$i[0]] = $i[1]
      | select($i[0] != "key2") // (
        .["key2a"] = "value2a" | .["key2b"] = "value2b"
      )
    )
    
    key1: value1
    key2: value2
    key2a: value2a
    key2b: value2b
    key3: value3
    

    However, until v4.27.5 (Sep 9, 2022) there was another bug in yq that wrongly evaluated assignments in the alternative branch (RHS of //) which eventually breaks this approach.

    Thus, for prior versions of yq, you could either replace the assignments with the adding of pre-formatted objects, or replace the whole alternative expression with the context operator with by abusing its root context for the condition:

    # Workaround for yq < v4.27.5, using object addition
    
    (.[] | [key, .]) as $i ireduce({}; .[$i[0]] = $i[1]
      | select($i[0] != "key2") // (
        . + {"key2a": "value2a", "key2b": "value2b"}
      )
    )
    
    # Workaround for yq < v4.27.5, using the `with` operator
    
    (.[] | [key, .]) as $i ireduce({}; .[$i[0]] = $i[1]
      | with(select($i[0] == "key2");
        .["key2a"] = "value2a" | .["key2b"] = "value2b"
      )
    )
    

    Note #2: All of these approaches also use the key operator, which was only introduced in v4.15.1 (Nov 24, 2021). Thus, for even prior versions of yq, you'd need other workarounds...


    With kislyuk/yq you do have the if expression available, but you can likewise just take the select shortcut. Also, you can simply return a comma-separated stream inside a map, which is internally called by with_entries.

    # Using `if`
    
    with_entries(if .key == "key2" then .,
      {key: "key2a", value: "value2a"},
      {key: "key2b", value: "value2b"}
    else . end)
    
    # Using `select`
    
    with_entries(., (select(.key == "key2") |
      {key: "key2a", value: "value2a"},
      {key: "key2b", value: "value2b"}
    ))
    

    With itchyny/gojq you'd probably have a hard time as it automatically sorts object keys by design. Thus, if your input file has keys in an unsorted order (e.g. key3, key1, key2), then even processing just the identity filter . would yield an output with its keys sorted (e.g. key1, key2, key3). Evidently, the same goes for any newly inserted keys.

    However, if your actual keys (including the ones to be inserted) are by chance in a sorted order anyway (like in your sample input, literally), you can exploit this circumstance by just primitively appending the new items at the end, and gojq will take care of inserting them at their sorted positions.