Ant: process elements in a list

I was looking for a way to process a list of items in an ant build file, similar to what you would do in Java with a construct like:

for ( Element element : elements ) {
    // do stuff with element
}

The approach of XSLT, using recursive calls with local variables, looked promising. In the ant-user mailing list, I found a posting on the topic Implementing a loop in ANT with something like what I was looking for. Ben Stringer’s example gave me the critical information—that I could make indirecly recursive calls to antcall. He also used an external file, updated with the buildnumber task, to maintain state through the recursive calls. Bingo!

The build file I describe here is available online.

To extract list elements, use the editstring macro described in an earlier post.

Rather than using buildnumber, I use the propertyfile task, which offers a means of passing information down through the recursion, and back to the caller. propertyfile is essentially a property file editor, and is ideal for my purposes.

Setup macros

Setting up the recursion involves

  • creating the list, and assigning it to a property,
  • writing to a property file the properties which will be required in the recursive call.

The setup process is performed by a series of nested macros. The inner macro is string-list, which accepts as attributes a string, the location of a properties file, the target to be antcalled for each list element, an optional flag to keep the properties file after processing, and an optional list separator string. The macro also requires that the element string-list-args be defined by the caller.

  
<macrodef name="string-list"
    description="List is a string. Default separator is path.separator.">
  <attribute name="properties"
      description="Location of the properties file."/>
  <attribute name="string"
      description="The string to edit."/>
  <attribute name="list.target"
      description="The target to run against each element of the list."/>
  <attribute name="keep-properties" default="false"
      description="Flag to keep the temporary properties file."/>
  <attribute name="separator" default="${path.separator}"
      description="Separator for elements of list."/>
  <element name="string-list-args"/>
  <sequential>
    <property name="list.keep.properties.file.."
        value="@{keep-properties}"/>
    <propertyfile file="@{properties}"
        comment="Automatically generated by string-list.">
      <entry key="list.counter.." type="int" value="0"/>
      <entry key="list" value="@{string}"/>
    </propertyfile>
    <string-list-args/>
    <antcall inheritall="false" target="list-initial__">
      <param name="properties" location="@{properties}"/>
      <param name="list.target" value="@{list.target}"/>
      <param name="separator" value="@{separator}"/>
    </antcall>
    <antcall inheritall="true" target="maybe-del-properties-file__"/>
  </sequential>
</macrodef>

Two other setup macros are provided: union-list and filelist-list. While string-list is general, and accepts any separator string, union-list and filelist-list are specifically for their eponymous types, and assume that ${path.separator} is the separator. filelist-list calls the union-list macro, and union-list calls string-list. Neither will be elaborated on here.

The recursive process

The recursion is started by an antcall to the target list-initial__, with inheritall=”false”. All properties are fed down through the properties file which has already been set up. That is the function of the dependency target list-set-properties__. It reads the properties into the context of this antcall instance. Because inheritall is false, all the properties defined in the file will take on the values in that properties file.

The list is split into the first element and the rest, and the properties file is updated with the first element and the new list.

<target name="list-initial__" depends="list-set-properties__"
  if="list.target" unless="list.finished">
  <!-- Split the list property into first and rest -->
  <editstring string="${list}" toproperty="first"
       pattern="^(.*?)(?&lt;!.*${separator}.*)(${separator}(.+))?$"
       replace="1"/>
  <editstring string="${list}" toproperty="rest"
       pattern="^(.*?)(?&lt;!.*${separator}.*)(${separator}(.+))?$"
       replace="3"/>
  <!-- Set up the properties file for the next (including first) iteration. -->
  <propertyfile file="${properties}">
    <entry key="list.first" value="${first}"/>
    <entry key="list" value="${rest}"/>
  </propertyfile>
  <!-- Start recursive processing -->
  <antcall inheritall="false" target="list-recur__"/>
</target>

The list-initial__ target finally calls list-recur__, again with inheritall=”false”. After reading the properties, and checking for the termination condition, the list element target ${list.target} is antcalled, but with inheritall=”true”, so that the current set of properties is available to it. The informational counter property list.counter.. is incremented in the properties file, and list-initial__ is re-invoked with inheritall=”false”.

<!-- Recursive process. Depends on:
  list-set-properties__
    loads the temporary properties file, containing the properties
    which must vary from invocation to invocation.
  list-recur1__
    checks for termination condition, setting termination flag property.
      
  If list not empty, list-recur__ runs the target against the per-invocation properties,
  then updates the element count in the ${properties} file.

  Finally it recursively calls list-initial__  -->
<target name="list-recur__" depends="list-set-properties__, maybe-list-finished__"
    unless="list.finished">
    <antcall inheritall="true" target="${list.target}"/>
    <propertyfile file="${properties}">
      <entry key="list.counter.." type="int" operation="+" value="1"/>
    </propertyfile>
    <antcall inheritall="false" target="list-initial__"/>
</target>

Usage

This build file, listutils.xml, expects to import editstring.xml from the same directory. It is not necessary to have listutils.xml in the same directory as your project. A build file to test listutils is available here. It also expects to find listutils.xml in the same directory. Here’s part of that file.

<target name="teststringlist">
  <property name="tmp" location="/tmp"/>
  <filelist id="flist1" dir="." files="mbox, errors, frames, frames.xml"/>
  <union id="union1">
    <filelist refid="flist1"/>
  </union>
  <property name="files1" value="${toString:union1}"/>
  <property name="properties" location="${tmp}/recursive.properties"/>
  <string-list properties="${properties}" string="${toString:union1}"
         list.target="list.target" keep-properties="true" separator="pbw">
    <string-list-args>
      <propertyfile file="@{properties}">
        <entry key="argument1" value="Argument 1 value"/>
        <entry key="argument2" value="Argument 2 value"/>
      </propertyfile>
    </string-list-args>
  </string-list>
</target>

<!-- The target to process each element of the list. -->
<target name="list.target">
  <echo>This list element is ${list.first}</echo>
  <echo>Arguments ${argument1} ${argument2}</echo>
</target>

teststringlist creates a filelist, from which it creates a union.  ${toString:<filelist>} does not give the desired result, but ${toString:<union>} returns a list with ${path.separator}s, which is the default separator. In this case, though, I am testing with an alternative separator, which will work for me because the files are all within my home directory, pbw. The other tests in this file assume ${path.separator}. These, of course, will break up the list differently.

Note the element string-list-args within the invocation of string-list. This is defined in the caller, and is expanded within the final expansion of string-list. In this case, and this would be typical, property definitions require in the list.target are added to the properties file, so that they will be available to the target.

The target for each element is here defined as list.target, which happens to be the same as the name of the property that passes it down. That is not necessary, however. In this case, the target simply echoes the list element name, and prints out the properties which have been provided through the string-list-args element.

Here’s the whole listutils.xml file.

<?xml version="1.0"?>
<?xml version="1.0"?>
<project name="listutils">

  <dirname property="listutils.dir" file="${ant.file.listutils}"/>
  <import file="${listutils.dir}/editstring.xml"/>
  
  <macrodef name="string-list"
      description="List is a string. Default separator is path.separator.">
    <attribute name="properties" description="Location of the properties file."/>
    <attribute name="string" description="The string to edit."/>
    <attribute name="list.target"
        description="The target to run against each element of the list."/>
    <attribute name="keep-properties" default="false"
        description="Flag to keep the temporary properties file."/>
    <attribute name="separator" default="${path.separator}"
        description="Separator for elements of list."/>
    <element name="string-list-args"/>
    <sequential>
      <property name="list.keep.properties.file.." value="@{keep-properties}"/>
      <propertyfile file="@{properties}" comment="Automatically generated by string-list.">
        <entry key="list.counter.." type="int" value="0"/>
        <entry key="list" value="@{string}"/>
      </propertyfile>
      <string-list-args/>
      <antcall inheritall="false" target="list-initial__">
        <param name="properties" location="@{properties}"/>
        <param name="list.target" value="@{list.target}"/>
        <param name="separator" value="@{separator}"/>
      </antcall>
      <antcall inheritall="true" target="maybe-del-properties-file__"/>
    </sequential>
  </macrodef>
  
  <macrodef name="union-list"
            description="List is result of ${toString:[union-id]}. Separator is path.separator.">
    <attribute name="properties" description="Location of the properties file."/>
    <attribute name="union-id" description="Id of a union containing the list."/>
    <attribute name="list.target"
               description="The target to run against each element of the list."/>
    <attribute name="keep-properties" default="false"
               description="Flag to keep the temporary properties file."/>
    <element name="union-list-args"/>
    <sequential>
      <string-list properties="@{properties}" string="${toString:@{union-id}}"
                   list.target="@{list.target}" keep-properties="@{keep-properties}">
        <string-list-args>
          <union-list-args/>
        </string-list-args>
      </string-list>
    </sequential>
  </macrodef>
  
  <macrodef name="filelist-list"
            description="List is a filelist. Must be converted to a union.">
    <attribute name="properties" description="Location of the properties file."/>
    <attribute name="filelist-id" description="Id of a filelist containing the list."/>
    <attribute name="list.target"
               description="The target to run against each element of the list."/>
    <attribute name="keep-properties" default="false"
               description="Flag to keep the temporary properties file."/>
    <element name="filelist-list-args"/>
    <sequential>
    <!-- The list is constructed by taking a <union> of a <filelist>.
        Union is required, because the ${toString:<propertyname>} expansion does
        not give the contents when applied to a <filelist>. When used on a <union>,
        it returns a text list separated by ${path.separator}.
    -->
      <union id="flist-union">
        <filelist refid="@{filelist-id}"/>
      </union>
      <union-list properties="@{properties}" union-id="flist-union"
                  list.target="@{list.target}" keep-properties="@{keep-properties}">
        <union-list-args>
          <filelist-list-args/>
        </union-list-args>
      </union-list>
    </sequential>
  </macrodef>

  <target name="list-element-count__">
    <property file="${properties}"/>
    <echo>Number of elements processed: ${list.counter..}</echo>
  </target>

  <!-- Clean up the temporary properties file. -->
  <target name="maybe-del-properties-file__" depends="list-element-count__"
          unless="${list.keep.properties.file..}">
    <delete file="${properties}"/>
  </target>

  <target name="list-initial__" depends="list-set-properties__" if="list.target" unless="list.finished">
    <!-- Split the list property into first and rest -->
    <editstring string="${list}" toproperty="first"
                   pattern="^(.*?)(?&lt;!.*${separator}.*)(${separator}(.+))?$"
                   replace="1"/>
    <editstring string="${list}" toproperty="rest"
                   pattern="^(.*?)(?&lt;!.*${separator}.*)(${separator}(.+))?$"
                   replace="3"/>
    <!-- Set up the properties file for the next (including first) iteration. -->
    <propertyfile file="${properties}">
      <entry key="list.first" value="${first}"/>
      <entry key="list" value="${rest}"/>
    </propertyfile>
    <!-- Start recursive processing -->
    <antcall inheritall="false" target="list-recur__"/>
  </target>

  <!-- Recursive process. Depends on:
      list-set-properties__ loads the temporary properties file, containing the properties
                            which must vary from invocation to invocation.
      list-recur1__ checks for termination condition, setting termination flag property.
      
      If list not empty, list-recur__ runs the target against the per-invocation properties,
      then updates the element count in the ${properties} file.

      Finally it recursively calls list-initial__  -->
  <target name="list-recur__" depends="list-set-properties__, maybe-list-finished__"
          unless="list.finished">
    <antcall inheritall="true" target="${list.target}"/>
    <propertyfile file="${properties}">
      <entry key="list.counter.." type="int" operation="+" value="1"/>
    </propertyfile>
    <antcall inheritall="false" target="list-initial__"/>
  </target>

  <!-- Load the property file to obtain the properties for this invocation -->
  <target name="list-set-properties__">
    <property file="${properties}"/>
  </target>

  <!-- Set the property list.finished if the list is exhausted. -->
  <target name="maybe-list-finished__">
    <condition property="list.finished">
      <equals arg1="${list.first}" arg2="$${rest}"/>
    </condition>
  </target>

</project>

Leave a Reply

Your email address will not be published. Required fields are marked *