Search code examples
arraysxmlsortinguniqueyq

How to add an element into inner xml attributes array and make sure it's sorted and unique


I'm working with firewalld XML file that looking like:

<?xml version="1.0" encoding="utf-8"?>
<zone>
  <short>Public</short>
  <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
  <service name="ssh"></service>
  <service name="dhcpv6-client"></service>
  <service name="cockpit"></service>
  <service name="https"></service>
  <service name="http"></service>
  <port port="5000" protocol="tcp"></port>
  <port port="5001" protocol="tcp"></port>
  <port port="9999" protocol="tcp"></port>
  <port port="9998" protocol="tcp"></port>
  <rule>
    <protocol value="vrrp"></protocol>
    <accept></accept>
  </rule>
</zone>

I want to add several new TCP ports into it, sort them and ensure they are unique


Solution

  • kislyuk/yq provides the xq command which can be used to manipulate XML files using jq syntax.

    For this case, just append another object (wrapped into an array) to the existing array, and apply unique to it (which also sorts the items). Note that the port numbers are processed as strings, so in order to have them sorted numerically, convert them for the comparisons using tonumber. Finally, the -x flag will tranform the result back into XML.

    xq -x '.zone.port |= (
      . + [{"@port": "993", "@protocol": "tcp"}]
      | unique_by(."@port" | tonumber)
    )' file.xml
    
    <zone>
      <short>Public</short>
      <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
      <service name="ssh"></service>
      <service name="dhcpv6-client"></service>
      <service name="cockpit"></service>
      <service name="https"></service>
      <service name="http"></service>
      <port port="993" protocol="tcp"></port>
      <port port="5000" protocol="tcp"></port>
      <port port="5001" protocol="tcp"></port>
      <port port="9998" protocol="tcp"></port>
      <port port="9999" protocol="tcp"></port>
      <rule>
        <protocol value="vrrp"></protocol>
        <accept></accept>
      </rule>
    </zone>
    

    If you want to add multiple entries at once, consider using --args to read them in as command-line arguments (available since jq 1.6):

    xq -x '.zone.port |= (
      . + [$ARGS.positional | _nwise(2) | {"@port": first, "@protocol": last}]
      | unique_by(."@port" | tonumber)
    )' file.xml --args 6543 tcp 210 udp
    
    <zone>
      <short>Public</short>
      <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
      <service name="ssh"></service>
      <service name="dhcpv6-client"></service>
      <service name="cockpit"></service>
      <service name="https"></service>
      <service name="http"></service>
      <port port="210" protocol="udp"></port>
      <port port="5000" protocol="tcp"></port>
      <port port="5001" protocol="tcp"></port>
      <port port="6543" protocol="tcp"></port>
      <port port="9998" protocol="tcp"></port>
      <port port="9999" protocol="tcp"></port>
      <rule>
        <protocol value="vrrp"></protocol>
        <accept></accept>
      </rule>
    </zone>