Ant Filters & Mappers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Ant 1.10.18. Every example was run on JDK 26 on 2026-10-16 with that release and the JARs in its lib directory, nothing else on the class path. The examples use the tidepool scenario of this section: the files come from its tidepool-model and tidepool-engine modules and from data/stations.csv, and the build file is a throwaway build.xml that is not part of the scenario itself. The absolute scratch path in console output was shortened to /work/tidepool.

Two Ant features rewrite the content or the name of a file while a task copies or loads it. Filters change what is inside the file: a @VERSION@ token becomes 1.4.0, comment lines disappear, lines are sorted. Mappers change where the result goes: Height.java becomes Height.java.bak, or a source path becomes a package name. Both are declarative building blocks that the task consumes, which is why they are data types and not tasks. The page covers the filterset type and the filter task, filterchains and their readers, mappers, and the regexp type that several of them share. Which files are selected in the first place is the subject of Paths, Filesets & Patterns and Selectors; the tasks that use filters and mappers are described in File Operations. Writing your own filter reader or mapper is on Custom Types, Conditions & Selectors.

Where filters and mappers sit in a copy

A task such as copy takes a set of files, optionally narrows it with selectors, reads every file through zero or more filters and writes it to a name computed by a mapper. The order is fixed by the task, not by the order of the nested elements in the build file.

flowchart LR fs["fileset: dir and patterns"] --> sel["selectors: filename, size, date"] sel --> fc["filterset and filterchain: change the content, line by line"] fc --> mp["mapper: compute the target name"] mp --> dst["destination: todir or tofile"] cf["filtering=true and global filter task"] -.-> fc

The dotted edge is the project-wide set of filters created by the filter task; it applies to a copy only when the copy has filtering="true". The same shape applies to move, and the filterchain half to concat, loadfile, loadproperties and loadresource as well.

The Ant manual says an Ant task "would not be able to accommodate such data transformation attributes" in the way a cat | head | grep pipeline composes them, and that is the design rationale: instead of one attribute per transformation on every task, the transformations are small readers that can be chained in any order, and a task only has to accept a <filterchain>.

The tidepool examples

Every example below is a target in one build.xml. Its header, which defines the output directory and a property that a later example expands, is:

<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool-filters" default="filterset" basedir=".">
  <property name="out" location="build"/>
  <property name="engine.name" value="harmonic"/>

  <target name="clean">
    <delete dir="${out}"/>
  </target>

The template processed by the first examples is src/main/resources/banner.txt.in of the CLI module:

# banner template, processed by the build
tidepool @VERSION@ (built @BUILD_DATE@)
stations file: @STATIONS@
engine: ${engine.name}
unknown token stays: @MISSING@

The files in this section are this page’s own setup for the examples that follow, not taken from the documentation.

Filtersets and the @token@ syntax

A filterset is a group of token and value pairs. Where a task accepts one, it reads each file line by line and replaces every @TOKEN@ with its value. The manual notes that the files are processed in text mode, so copying a binary file through a filterset typically corrupts it: select only text files.

Attribute Meaning Default

begintoken

String that opens a token.

@

endtoken

String that closes a token.

@

filtersfile

One properties file whose entries are tokens and values.

none

recurse

Whether the replacement text is searched for more tokens (Ant 1.6.3).

true

onmissingfiltersfile

What to do when a filtersfile does not exist: fail, warn or ignore (Ant 1.7).

fail

refid

Makes the element a reference to a filterset defined elsewhere; no other attribute or nested element is then allowed.

none

The nested elements are <filter token="..." value="..."/>, <filtersfile file="..."/> (a properties file of names and values) and <propertyset>, whose properties become tokens when the filterset is created (a <propertyref name="project.date"/> supplies @project.date@). A filterset may contain other filtersets, which gives the union of their filters, and it can carry an id so that it is declared once at project level and referenced with refid afterwards. Within one filterset the last value defined for a token wins.

<target name="filterset" depends="clean">
  <tstamp><format property="build.date" pattern="yyyy-MM-dd"/></tstamp>
  <copy file="src/main/resources/banner.txt.in" tofile="${out}/banner.txt">
    <filterset>
      <filter token="VERSION" value="1.4.0"/>
      <filter token="BUILD_DATE" value="${build.date}"/>
      <filter token="STATIONS" value="data/stations.csv"/>
    </filterset>
  </copy>
  <loadfile srcfile="${out}/banner.txt" property="banner"/>
  <echo message="${banner}"/>
  <!-- other delimiters, tokens from a file -->
  <copy file="src/main/resources/release.properties.in" tofile="${out}/release.properties">
    <filterset begintoken="%" endtoken="*">
      <filtersfile file="filters.properties"/>
    </filterset>
  </copy>
  <loadfile srcfile="${out}/release.properties" property="release"/>
  <echo message="${release}"/>
</target>

Running it:

$ ant -q filterset
     [echo] # banner template, processed by the build
     [echo] tidepool 1.4.0 (built 2026-10-16)
     [echo] stations file: data/stations.csv
     [echo] engine: ${engine.name}
     [echo] unknown token stays: @MISSING@
     [echo] version=1.4.0
     [echo] codename=spring-tide
BUILD SUCCESSFUL
Total time: 0 seconds

The @MISSING@ token, which no filter defines, and the ${engine.name} property reference are copied unchanged: a filterset knows only tokens, and Ant does not expand ${...} inside file content that a task copies. The second copy uses % and * as delimiters, which matter when the file format already uses @ (shell scripts, email addresses, Java annotations). Choose delimiters that cannot appear in the text for any other reason, because a stray pair of delimiters around an undefined name is left alone, but a name that is defined is replaced wherever it is found.

Nested tokens: recurse

Because recurse defaults to true, a replacement value that itself contains a token is expanded again:

<target name="recurse" depends="clean">
  <echo file="${out}/t.txt" message="@A@${line.separator}"/>
  <copy file="${out}/t.txt" tofile="${out}/recursive.txt">
    <filterset><filter token="A" value="x-@B@-x"/><filter token="B" value="inner"/></filterset>
  </copy>
  <copy file="${out}/t.txt" tofile="${out}/flat.txt">
    <filterset recurse="false"><filter token="A" value="x-@B@-x"/><filter token="B" value="inner"/></filterset>
  </copy>
  <loadfile srcfile="${out}/recursive.txt" property="r1"/>
  <loadfile srcfile="${out}/flat.txt" property="r2"/>
  <echo message="recurse=true : ${r1}"/>
  <echo message="recurse=false: ${r2}"/>
</target>
$ ant -q recurse
     [echo] recurse=true : x-inner-x
     [echo] recurse=false: x-@B@-x
BUILD SUCCESSFUL
Total time: 0 seconds

Set recurse="false" when values may legitimately contain the delimiters, such as a Windows path that includes @, or an email address.

A missing filters file

An absent filtersfile stops the build by default. The message names the file:

<target name="bad-filtersfile">
  <copy file="data/stations.csv" tofile="${out}/x.csv"><filterset><filtersfile file="nope.properties"/></filterset></copy>
</target>
$ ant -q bad-filtersfile
BUILD FAILED
/work/tidepool/build.xml:289: Could not read filters from file /work/tidepool/nope.properties as it doesn't exist.
Total time: 0 seconds
$ echo $?
1

onmissingfiltersfile="warn" logs a warning and continues, ignore continues silently; use them for optional, developer-specific overrides, never to hide a mistyped name.

The filter task: project-wide filters

The filter task defines filters for the whole project instead of for one task. It takes either a token and a value or a filtersfile, and uses @ as the delimiter; a token string that contains the separator is ignored. Those filters are used by every copy operation that has filtering="true", and by none of the others.

<target name="filter-task" depends="clean">
  <filter token="VERSION" value="1.4.0"/>
  <filter token="BUILD_DATE" value="today"/>
  <filter token="STATIONS" value="data/stations.csv"/>
  <copy file="src/main/resources/banner.txt.in" tofile="${out}/plain.txt"/>
  <copy file="src/main/resources/banner.txt.in" tofile="${out}/filtered.txt" filtering="true"/>
  <loadfile srcfile="${out}/plain.txt" property="plain"/>
  <loadfile srcfile="${out}/filtered.txt" property="filtered"/>
  <echo message="plain.txt:${line.separator}${plain}"/>
  <echo message="filtered.txt:${line.separator}${filtered}"/>
</target>
$ ant -q filter-task
     [echo] plain.txt:
     [echo] # banner template, processed by the build
     [echo] tidepool @VERSION@ (built @BUILD_DATE@)
     [echo] stations file: @STATIONS@
     [echo] engine: ${engine.name}
     [echo] unknown token stays: @MISSING@
     [echo] filtered.txt:
     [echo] # banner template, processed by the build
     [echo] tidepool 1.4.0 (built today)
     [echo] stations file: data/stations.csv
     [echo] engine: ${engine.name}
     [echo] unknown token stays: @MISSING@
BUILD SUCCESSFUL
Total time: 0 seconds

plain.txt is untouched and filtered.txt has its tokens replaced. Because a project-wide filter reaches every filtering="true" copy in the build, including copies made by imported build files, prefer a filterset on the task that needs it; reserve filter for a value that is genuinely global, such as the version.

Filterchains and filter readers

A filterchain is an ordered group of filter readers. Each reader is a java.io.FilterReader, and the output of one is the input of the next, so the order of the elements is the order of the transformations. The tasks that accept a <filterchain> are concat, copy, loadfile, loadproperties, loadresource and move. The only attribute is refid, which makes the element a reference to a filterchain declared elsewhere, for example with an id at project level.

Every reader has two spellings. The generic form names the class and passes parameters, and is the only form that accepts a reader of your own:

<filterchain>
  <filterreader classname="org.apache.tools.ant.filters.HeadFilter">
    <param name="lines" value="15"/>
  </filterreader>
</filterchain>

The convenience form is a dedicated element with attributes, <headfilter lines="15"/>. A <filterreader> also accepts a nested <classpath>; a class that is parameterised must implement org.apache.tools.ant.types.Parameterizable, and the class must extend java.io.FilterReader.

The built-in readers

Reader What it does and its parameters Since

expandproperties

Replaces ${name} with the value of the property. A nested <propertyset> restricts the properties used.

1.8.3 for the propertyset

replacetokens

Replaces @TOKEN@ strings. begintoken and endtoken change the delimiters, <token key="..." value="..."/> defines a token, and propertiesfile or propertiesResource read tokens from a file or an Ant resource.

1.8.0 for propertiesResource

headfilter

First lines lines (default 10); skip lines are skipped from the start; a negative lines passes everything.

tailfilter

Last lines lines (default 10); skip lines are skipped from the end; a negative lines passes everything.

linecontains

Keeps lines that contain every nested <contains value="..."/> string. negate="true" keeps the others; matchAny="true" accepts a line that contains any of the strings.

1.7 negate, 1.10.4 matchAny

linecontainsregexp

Keeps lines matching a regular expression, given as a nested <regexp pattern="..."/> or the regexp attribute. negate inverts it and casesensitive="false" ignores case.

1.7 negate, 1.8.2 casesensitive, 1.10.2 regexp attribute

prefixlines

Attaches prefix to every line.

suffixlines

Attaches suffix to every line.

1.8.0

striplinecomments

Removes lines that begin with one of the nested <comment value="..."/> strings.

stripjavacomments

Removes Java style comments, both // and /* */. No parameters.

striplinebreaks

Removes the characters in linebreaks (default \r\n), which joins the lines.

tabstospaces

Replaces each tab with tablength spaces (default 8).

deletecharacters

Deletes the characters in chars (backslash escapes allowed). Available only in the convenience form.

1.6

escapeunicode

Converts every non US-ASCII character to a backslash-u escape.

1.6

concatfilter

Prepends the content of the prepend file or appends the append file.

1.6

sortfilter

Reads all lines and sorts them; reverse="true" reverses the order, and a nested comparator implementation (class name in the comparator parameter) changes it.

1.8.0

tokenfilter

Splits the input into tokens and passes them through string filters; described below.

1.6

classconstants

Prints the basic constants of a Java class file as name=value lines. Uses the BCEL library, and the file must be read with encoding="ISO-8859-1" because it operates on bytes.

fixcrlf

The fixcrlf task used as a reader: line endings, tabs and end-of-file characters (its manual page lists which attributes apply as a filter).

1.7

The two readers to learn first are replacetokens, for the same job as a filterset but inside a chain, and expandproperties, for a file that contains ${property} references. A chain can use both on one file, which a filterset alone cannot:

<target name="chain-basic" depends="clean">
  <copy file="src/main/resources/banner.txt.in" tofile="${out}/banner.txt">
    <filterchain>
      <striplinecomments><comment value="#"/></striplinecomments>
      <replacetokens>
        <token key="VERSION" value="1.4.0"/>
        <token key="BUILD_DATE" value="2026-10-16"/>
        <token key="STATIONS" value="data/stations.csv"/>
      </replacetokens>
      <expandproperties/>
      <prefixlines prefix="| "/>
    </filterchain>
  </copy>
  <loadfile srcfile="${out}/banner.txt" property="banner"/>
  <echo message="${banner}"/>
</target>
$ ant -q chain-basic
     [echo] | tidepool 1.4.0 (built 2026-10-16)
     [echo] | stations file: data/stations.csv
     [echo] | engine: harmonic
     [echo] | unknown token stays: @MISSING@
BUILD SUCCESSFUL
Total time: 0 seconds

striplinecomments runs first, so the template’s # header is gone; replacetokens then fills in the tokens; expandproperties expands ${engine.name} using the property from the header of the build file; and prefixlines decorates what is left. The @MISSING@ token stays because no <token> defines it. Reorder the elements and the result changes, for example prefixlines before striplinecomments would prefix the comment line so that it no longer begins with #.

Selecting lines

The line readers work on data/stations.csv, loaded into a property with loadfile so that the output can be echoed:

<target name="chain-lines" depends="clean">
  <loadfile srcfile="data/stations.csv" property="head2"><filterchain><headfilter lines="2"/></filterchain></loadfile>
  <loadfile srcfile="data/stations.csv" property="body"><filterchain><headfilter lines="-1" skip="1"/></filterchain></loadfile>
  <loadfile srcfile="data/stations.csv" property="tail1"><filterchain><tailfilter lines="1"/></filterchain></loadfile>
  <loadfile srcfile="data/stations.csv" property="m2"><filterchain><linecontains><contains value="M2"/><contains value="S2"/></linecontains></filterchain></loadfile>
  <loadfile srcfile="data/stations.csv" property="without.k1"><filterchain><linecontains negate="true"><contains value="K1"/></linecontains></filterchain></loadfile>
  <loadfile srcfile="data/stations.csv" property="rx"><filterchain><linecontainsregexp regexp="^(SF|HNL),"/></filterchain></loadfile>
  <loadfile srcfile="data/stations.csv" property="suffixed"><filterchain><headfilter lines="2" skip="1"/><suffixlines suffix=" ;"/></filterchain></loadfile>
  <echo message="headfilter lines=2:${line.separator}${head2}"/>
  <echo message="headfilter lines=-1 skip=1:${line.separator}${body}"/>
  <echo message="tailfilter lines=1:${line.separator}${tail1}"/>
  <echo message="linecontains M2 and S2:${line.separator}${m2}"/>
  <echo message="linecontains negate K1:${line.separator}${without.k1}"/>
  <echo message="linecontainsregexp:${line.separator}${rx}"/>
  <echo message="head skip + suffixlines:${line.separator}${suffixed}"/>
</target>
$ ant -q chain-lines
     [echo] headfilter lines=2:
     [echo] id,name,latitude,longitude,constituents
     [echo] SF,San Francisco,37.8063,-122.4659,M2:0.55:120;S2:0.13:150;K1:0.38:210
     [echo] headfilter lines=-1 skip=1:
     [echo] SF,San Francisco,37.8063,-122.4659,M2:0.55:120;S2:0.13:150;K1:0.38:210
     [echo] BOS,Boston,42.3539,-71.0503,M2:1.35:15;S2:0.21:42;N2:0.28:350
     [echo] HNL,Honolulu,21.3067,-157.8645,M2:0.14:90;K1:0.17:250;O1:0.10:230
     [echo] tailfilter lines=1:
     [echo] HNL,Honolulu,21.3067,-157.8645,M2:0.14:90;K1:0.17:250;O1:0.10:230
     [echo] linecontains M2 and S2:
     [echo] SF,San Francisco,37.8063,-122.4659,M2:0.55:120;S2:0.13:150;K1:0.38:210
     [echo] BOS,Boston,42.3539,-71.0503,M2:1.35:15;S2:0.21:42;N2:0.28:350
     [echo] linecontains negate K1:
     [echo] id,name,latitude,longitude,constituents
     [echo] BOS,Boston,42.3539,-71.0503,M2:1.35:15;S2:0.21:42;N2:0.28:350
     [echo] linecontainsregexp:
     [echo] SF,San Francisco,37.8063,-122.4659,M2:0.55:120;S2:0.13:150;K1:0.38:210
     [echo] HNL,Honolulu,21.3067,-157.8645,M2:0.14:90;K1:0.17:250;O1:0.10:230
     [echo] head skip + suffixlines:
     [echo] SF,San Francisco,37.8063,-122.4659,M2:0.55:120;S2:0.13:150;K1:0.38:210 ;
     [echo] BOS,Boston,42.3539,-71.0503,M2:1.35:15;S2:0.21:42;N2:0.28:350 ;
BUILD SUCCESSFUL
Total time: 0 seconds

headfilter lines="-1" skip="1" drops the header line, the idiom for "everything but the first line"; tailfilter mirrors it for the end of the file. linecontains with two <contains> children keeps a line only if it has both strings (the three-constituent San Francisco and Boston lines). Combining headfilter and tailfilter extracts a range of lines, as the manual’s picture of a 99-line file shows.

Comments, tabs, line breaks and non-ASCII text

<target name="chain-text" depends="clean">
  <loadfile srcfile="src/main/java/io/tidepool/model/Height.java" property="nocomments">
    <filterchain><stripjavacomments/></filterchain>
  </loadfile>
  <echo message="stripjavacomments:${line.separator}${nocomments}"/>
  <loadfile srcfile="noisy.txt" property="noisy" encoding="UTF-8">
    <filterchain>
      <striplinecomments>
        <comment value="#"/><comment value="--"/><comment value="REM "/>
      </striplinecomments>
      <tabstospaces tablength="2"/>
      <escapeunicode/>
    </filterchain>
  </loadfile>
  <echo message="striplinecomments + tabstospaces + escapeunicode:${line.separator}${noisy}"/>
  <loadfile srcfile="constituents.txt" property="oneline">
    <filterchain><striplinebreaks/></filterchain>
  </loadfile>
  <echo message="striplinebreaks: ${oneline}"/>
  <loadfile srcfile="constituents.txt" property="nom2">
    <filterchain><deletecharacters chars="2"/></filterchain>
  </loadfile>
  <echo message="deletecharacters chars=2:${line.separator}${nom2}"/>
</target>

The input noisy.txt is a UTF-8 file with a # comment, -- and REM ` comment lines, a tab-indented line and one accented character, and `Height.java carries // and /* */ comments:

$ ant -q chain-text
     [echo] stripjavacomments:
     [echo] package io.tidepool.model;
     [echo]
     [echo]
     [echo]
     [echo] public record Height(double metres) {
     [echo]
     [echo]     public static final String DATUM = "LAT";
     [echo] }
     [echo] striplinecomments + tabstospaces + escapeunicode:
     [echo] name=Plage de l\u00e9on
     [echo]
     [echo]      indented  line
     [echo] value=1
     [echo] striplinebreaks: M2S2K1M2M2O1S2
     [echo] deletecharacters chars=2:
     [echo] M
     [echo] S
     [echo] K1
     [echo] M
     [echo] M
     [echo] O1
     [echo] S
BUILD SUCCESSFUL
Total time: 0 seconds

stripjavacomments removes the comment text but leaves the line breaks, so blank lines remain. escapeunicode turned the accented letter into \u00e9, the form a Java .properties file requires. The -- sql comment and REM batch comment lines are gone because each prefix was declared as a <comment>. When a file has a non-default encoding, set it on the loading or copying task (encoding="UTF-8" here), not on the filter: the reader sees characters, not bytes.

Sorting and removing duplicates

<target name="chain-sort" depends="clean">
  <loadfile srcfile="constituents.txt" property="sorted">
    <filterchain><sortfilter/></filterchain>
  </loadfile>
  <loadfile srcfile="constituents.txt" property="rsorted">
    <filterchain><sortfilter reverse="true"/><tokenfilter><uniqfilter/></tokenfilter></filterchain>
  </loadfile>
  <loadfile srcfile="constituents.txt" property="uniq">
    <filterchain><sortfilter/><uniqfilter/></filterchain>
  </loadfile>
  <echo message="sortfilter:${line.separator}${sorted}"/>
  <echo message="sortfilter reverse=true, then uniqfilter:${line.separator}${rsorted}"/>
  <echo message="sortfilter then uniqfilter:${line.separator}${uniq}"/>
</target>
$ ant -q chain-sort
     [echo] sortfilter:
     [echo] K1
     [echo] M2
     [echo] M2
     [echo] M2
     [echo] O1
     [echo] S2
     [echo] S2
     [echo] sortfilter reverse=true, then uniqfilter:
     [echo] S2
     [echo] O1
     [echo] M2
     [echo] K1
     [echo] sortfilter then uniqfilter:
     [echo] K1
     [echo] M2
     [echo] O1
     [echo] S2
BUILD SUCCESSFUL
Total time: 0 seconds

uniqfilter only suppresses a token equal to the one before it, so it is useful only after a sort, as the manual says. The second result combines sortfilter reverse="true" with a tokenfilter that contains the uniqfilter, which is the form to use when the uniqueness belongs in a tokenfilter chain. sortfilter holds all the lines in memory.

tokenfilter: strings, tokenizers and string filters

tokenfilter splits its input into tokens with one tokenizer and passes each token through string filters. The default tokenizer is linetokenizer, so a <tokenfilter> without a tokenizer works line by line. The others are filetokenizer (the whole input is one token, so do not use it on large files) and stringtokenizer (split on whitespace or on the delims characters; suppressdelims, delimsaretokens and includeDelims control the delimiters). The string filters are replacestring, containsstring, replaceregex, containsregex, trim, ignoreblank, deletecharacters, uniqfilter and native2asciifilter (Ant 1.9.8), plus the optional scriptfilter. A string filter returns a string or null, and null drops the token. delimOutput replaces the delimiter that is written after each token and understands the backslash escapes \n, \r, \f, \t and \\. Some readers can also be written directly in a filterchain, in which case a tokenfilter is created implicitly and an extra byline attribute selects line or whole-file processing.

<target name="chain-token" depends="clean">
  <loadfile srcfile="data/stations.csv" property="tok1">
    <filterchain>
      <tokenfilter>
        <containsregex pattern="^([A-Z]+),([^,]+),.*$" replace="\1 = \2"/>
      </tokenfilter>
    </filterchain>
  </loadfile>
  <echo message="containsregex with replace (header dropped):${line.separator}${tok1}"/>
  <echo file="${out}/words.txt" message="M2 S2 K1${line.separator}"/>
  <loadfile srcfile="${out}/words.txt" property="tok2">
    <filterchain>
      <tokenfilter>
        <stringtokenizer/>
        <replaceregex pattern="(.+)" replace="[\1]"/>
      </tokenfilter>
    </filterchain>
  </loadfile>
  <echo message="stringtokenizer with replaceregex: ${tok2}"/>
  <loadfile srcfile="noisy.txt" property="tok5" encoding="UTF-8">
    <filterchain>
      <headfilter lines="2" skip="1"/>
      <tokenfilter><native2asciifilter/></tokenfilter>
    </filterchain>
  </loadfile>
  <echo message="native2asciifilter: ${tok5}"/>
  <loadfile srcfile="data/stations.csv" property="tok3">
    <filterchain>
      <headfilter lines="2" skip="1"/>
      <tokenfilter>
        <replacestring from="San Francisco" to="SF City"/>
        <trim/>
        <containsstring contains="City"/>
      </tokenfilter>
    </filterchain>
  </loadfile>
  <echo message="replacestring, trim, containsstring:${line.separator}${tok3}"/>
  <loadfile srcfile="noisy.txt" property="tok4" encoding="UTF-8">
    <filterchain>
      <tokenfilter><deletecharacters chars="\t"/><trim/><ignoreblank/></tokenfilter>
    </filterchain>
  </loadfile>
  <echo message="deletecharacters, trim, ignoreblank:${line.separator}${tok4}"/>
</target>
$ ant -q chain-token
     [echo] containsregex with replace (header dropped):
     [echo] SF = San Francisco
     [echo] BOS = Boston
     [echo] HNL = Honolulu
     [echo] stringtokenizer with replaceregex: [M2] [S2] [K1]
     [echo] native2asciifilter: name=Plage de l\u00e9on
     [echo]
     [echo] replacestring, trim, containsstring:
     [echo] SF,SF City,37.8063,-122.4659,M2:0.55:120;S2:0.13:150;K1:0.38:210
     [echo] deletecharacters, trim, ignoreblank:
     [echo] # comment line
     [echo] name=Plage de léon
     [echo] indentedline
     [echo] -- sql comment
     [echo] REM batch comment
     [echo] value=1
BUILD SUCCESSFUL
Total time: 0 seconds

The first filter used containsregex with a replacement, which both selects and rewrites: the header line does not match the pattern and is dropped, and each station becomes ID = name. The second splits M2 S2 K1 on whitespace and wraps each word in brackets; the third applies native2asciifilter to a line with an accented character, with the same result as escapeunicode; the fourth chains three string filters; the fifth deletes tabs, trims and removes blank lines, which is why indented<TAB>line became indentedline.

Custom tokenizers and string filters implement org.apache.tools.ant.util.Tokenizer and org.apache.tools.ant.filters.TokenFilter.Filter, and are registered with typedef; the manual’s Capitalize example is the template.

Reusing a filterchain and adding a header

A filterchain with an id is a project-level definition, referenced with refid. concatfilter prepends a file to every file it filters, here a generated-code notice to a copy of Height.java:

<target name="chain-ref" depends="clean">
  <echo file="${out}/header.txt" message="// generated by tidepool${line.separator}"/>
  <filterchain id="java.header">
    <concatfilter prepend="${out}/header.txt"/>
  </filterchain>
  <copy todir="${out}/gen">
    <fileset dir="src/main/java" includes="**/Height.java"/>
    <filterchain refid="java.header"/>
  </copy>
  <loadfile srcfile="${out}/gen/io/tidepool/model/Height.java" property="gen"/>
  <echo message="${gen}"/>
</target>
$ ant -q chain-ref
     [echo] // generated by tidepool
     [echo] package io.tidepool.model;
     [echo]
     [echo] // Licence header: internal use only.
     [echo] /** A tide height in metres above chart datum. */
     [echo] public record Height(double metres) {
     [echo]     /* the chart datum is lowest astronomical tide */
     [echo]     public static final String DATUM = "LAT"; // keep in sync with the CLI
     [echo] }
BUILD SUCCESSFUL
Total time: 0 seconds

scriptfilter and the missing JavaScript engine

scriptfilter runs a script for every token, with a self object that has getToken() and setToken(String). It is an optional reader that needs a JSR 223 or Apache BSF script engine. The manual’s own example uses javascript, which no longer ships with the JDK since Nashorn was removed in Java 15, so on JDK 26 it fails unless an engine such as GraalVM JavaScript is added to the class path:

<target name="scriptfilter" depends="clean">
  <copy file="data/stations.csv" tofile="${out}/upper.csv">
    <filterchain>
      <tokenfilter>
        <scriptfilter language="javascript">self.setToken(self.getToken().toUpperCase());</scriptfilter>
      </tokenfilter>
    </filterchain>
  </copy>
</target>
$ ant -q scriptfilter
Java 15 has removed Nashorn, you must provide an engine for running JavaScript yourself. GraalVM JavaScript currently is the preferred option.
BUILD FAILED
/work/tidepool/build.xml:184: Unable to create javax script engine for javascript
Total time: 0 seconds
$ echo $?
1

See Scripting for how to supply an engine (<script>, <classpath>, manager). Every script-based element needs the same engine, so scriptmapper and scriptcondition (the latter is run on Conditions & Control Flow) fail in the same way on a bare JDK. A string filter written in Java, or a tokenfilter chain, avoids the dependency.

Not run here: classconstants

classconstants needs the BCEL library next to ant-apache-bcel.jar, and BCEL is not part of the Ant distribution or of the JDK, so the example in the manual was checked against its description but not run for this page. Add the BCEL JAR to ANT_HOME/lib or to ~/.ant/lib (see Installation) and load the class file with encoding="ISO-8859-1".

Mappers

A task that creates target files from source files does not take the target names directly. It asks a mapper: an instance of org.apache.tools.ant.util.FileNameMapper that returns zero, one or several target names for a source name. A mapper that returns nothing for a source means that file is skipped. Mappers take from and to arguments whose exact meaning depends on the implementation, and tasks use them in copy, move, uptodate, pathconvert and apply, and in the mappedresources resource collection.

A <mapper> element has these attributes:

Attribute Meaning

type

One of the built-in implementation names below. Exactly one of type and classname is required.

classname

Class name of an implementation, with classpath or classpathref, or a nested <classpath>.

from, to

Arguments for the implementation.

refid

Reference to a mapper defined elsewhere.

Since Ant 1.7.0 every built-in implementation also has a dedicated element (<globmapper>, <flattenmapper>) that can carry additional attributes, and nested mappers are implicitly wrapped in a composite mapper. The <mapper type="..."> form still works. All built-in mappers are case sensitive, and Ant does not convert / and \ in from and to to the platform separator: use ${file.separator}, or handledirsep="true" on the glob and regexp mappers.

A helper to see what a mapper does

To show the effect of each mapper without copying files, the build uses a macro that feeds a fileset through a mapper with pathconvert and prints the resulting names. Names that a mapper does not map are simply absent from the output. See Macros & Presets for macrodef.

<macrodef name="show-mapping">
  <attribute name="title"/>
  <element name="m" implicit="true"/>
  <sequential>
    <pathconvert property="mapped.@{title}" pathsep="${line.separator}  " setonempty="false">
      <fileset dir="src">
        <include name="main/java/io/tidepool/model/Height.java"/>
        <include name="main/java/io/tidepool/engine/TideEngine.java"/>
        <include name="test/java/io/tidepool/model/HeightTest.java"/>
        <include name="main/resources/banner.txt.in"/>
      </fileset>
      <chainedmapper>
        <globmapper from="${basedir}${file.separator}src${file.separator}*" to="*"/>
        <m/>
      </chainedmapper>
    </pathconvert>
    <echo message="@{title}:${line.separator}  ${mapped.@{title}}"/>
  </sequential>
</macrodef>

The four source files are main/java/io/tidepool/engine/TideEngine.java, main/java/io/tidepool/model/Height.java, test/java/io/tidepool/model/HeightTest.java and main/resources/banner.txt.in, relative to src.

<target name="mappers">
  <show-mapping title="identity"><identitymapper/></show-mapping>
  <show-mapping title="flatten"><flattenmapper/></show-mapping>
  <show-mapping title="merge"><mergemapper to="all.txt"/></show-mapping>
  <show-mapping title="glob"><globmapper from="*.java" to="*.java.bak"/></show-mapping>
  <show-mapping title="regexp"><regexpmapper from="^(.*)/([^/]+)/([^/]*)$$" to="\1/\2/\2-\3"/></show-mapping>
  <show-mapping title="package"><packagemapper from="main/java/*.java" to="*.src"/></show-mapping>
  <show-mapping title="unpackage"><chainedmapper><packagemapper from="main/java/*.java" to="*"/><unpackagemapper from="*" to="gen/*.txt"/></chainedmapper></show-mapping>
  <show-mapping title="composite"><compositemapper><identitymapper/><packagemapper from="main/java/*.java" to="*"/></compositemapper></show-mapping>
  <show-mapping title="chained"><chainedmapper><flattenmapper/><globmapper from="*" to="new/path/*"/></chainedmapper></show-mapping>
  <show-mapping title="firstmatch"><firstmatchmapper><globmapper from="*Test.java" to="*Test.xml"/><globmapper from="*.java" to="*.class"/></firstmatchmapper></show-mapping>
  <show-mapping title="cutdirs"><cutdirsmapper dirs="3"/></show-mapping>
  <show-mapping title="filtermapper"><filtermapper><replacestring from="/" to="-"/></filtermapper></show-mapping>
</target>
$ ant -q mappers
     [echo] identity:
     [echo]   main/java/io/tidepool/engine/TideEngine.java
     [echo]   main/java/io/tidepool/model/Height.java
     [echo]   main/resources/banner.txt.in
     [echo]   test/java/io/tidepool/model/HeightTest.java
     [echo] flatten:
     [echo]   TideEngine.java
     [echo]   Height.java
     [echo]   banner.txt.in
     [echo]   HeightTest.java
     [echo] merge:
     [echo]   all.txt
     [echo]   all.txt
     [echo]   all.txt
     [echo]   all.txt
     [echo] glob:
     [echo]   main/java/io/tidepool/engine/TideEngine.java.bak
     [echo]   main/java/io/tidepool/model/Height.java.bak
     [echo]   test/java/io/tidepool/model/HeightTest.java.bak
     [echo] regexp:
     [echo]   main/java/io/tidepool/engine/engine-TideEngine.java
     [echo]   main/java/io/tidepool/model/model-Height.java
     [echo]   main/resources/resources-banner.txt.in
     [echo]   test/java/io/tidepool/model/model-HeightTest.java
     [echo] package:
     [echo]   io.tidepool.engine.TideEngine.src
     [echo]   io.tidepool.model.Height.src
     [echo] unpackage:
     [echo]   gen/io/tidepool/engine/TideEngine.txt
     [echo]   gen/io/tidepool/model/Height.txt
     [echo] composite:
     [echo]   main/java/io/tidepool/engine/TideEngine.java
     [echo]   io.tidepool.engine.TideEngine
     [echo]   main/java/io/tidepool/model/Height.java
     [echo]   io.tidepool.model.Height
     [echo]   main/resources/banner.txt.in
     [echo]   test/java/io/tidepool/model/HeightTest.java
     [echo] chained:
     [echo]   new/path/TideEngine.java
     [echo]   new/path/Height.java
     [echo]   new/path/banner.txt.in
     [echo]   new/path/HeightTest.java
     [echo] firstmatch:
     [echo]   main/java/io/tidepool/engine/TideEngine.class
     [echo]   main/java/io/tidepool/model/Height.class
     [echo]   test/java/io/tidepool/model/HeightTest.xml
     [echo] cutdirs:
     [echo]   tidepool/engine/TideEngine.java
     [echo]   tidepool/model/Height.java
     [echo]   tidepool/model/HeightTest.java
     [echo] filtermapper:
     [echo]   main-java-io-tidepool-engine-TideEngine.java
     [echo]   main-java-io-tidepool-model-Height.java
     [echo]   main-resources-banner.txt.in
     [echo]   test-java-io-tidepool-model-HeightTest.java
BUILD SUCCESSFUL
Total time: 0 seconds

The mappers one by one

Mapper from and to Behaviour Since

identity

ignored

The target name equals the source name.

flatten

ignored

The target name is the source name without its directories.

merge

to only

Every source maps to the same target name. Used, for example, with uptodate to compare many sources with one target.

glob

both, one * at most

The text matched by * in from is substituted into to. Names that do not match are ignored. casesensitive and handledirsep attributes.

casesensitive and handledirsep 1.6.3

regexp

both, regular expressions

Back-references \0 to \9 refer to the match and the parenthesised groups; to is the whole new name, so replacing an extension needs (.*)\.old$, not \.old$. A dollar sign is written $$ in a build file. casesensitive and handledirsep attributes.

casesensitive and handledirsep 1.6.3

package

both, glob syntax

Like glob, but directory separators found in the matched part become dots. Typical use: a test class path to a JUnit report name.

unpackage

both, glob syntax

The inverse of package: dots in the matched text become directory separators.

1.6.0

composite

none, nested mappers

Passes the name to every nested mapper and returns all the results, in nested order.

1.7.0, ordered since 1.8.0

chained

none, nested mappers

Pipes the name through the nested mappers: the results of one are the inputs of the next.

1.7.0

filtermapper

none, nested filter readers

Applies a filterchain to the name.

1.6.3

scriptmapper

language, script

Runs a script once per source; self.addMappedName(...) adds a target name and source is the name. Needs a script engine.

1.7

firstmatchmapper

none, nested mappers

Returns the result of the first nested mapper that matches, unlike composite, which collects all.

1.8.0

cutdirsmapper

dirs

Strips that many leading directories from the name.

1.8.2

The merge output above is the same name four times, which is how a task learns that many sources feed one target. The regexp mapper turned main/java/io/tidepool/model/Height.java into main/java/io/tidepool/model/model-Height.java by prefixing the file with the name of its directory, and the package and unpackage pairs round-trip between paths and Java-style names. In the unpackage line a chained mapper first converted the path to a package name and then back to a path with a .txt extension. The cutdirs result has no banner.txt.in line because that name has fewer than three directories, so it has nothing to cut and is skipped. The firstmatch result shows the difference with composite: the Test pattern wins for HeightTest.java, so it is never turned into a .class name.

Silent failures

Mappers are forgiving, and the build does not tell you when one does nothing. A glob mapper that has no to attribute does not fail; every file that matches from is written to a file called null:

<target name="glob-without-to" depends="clean">
  <copy todir="${out}/x">
    <fileset dir="data"/>
    <globmapper from="*.csv"/>
  </copy>
  <pathconvert property="made" pathsep=", ">
    <fileset dir="${out}/x"/>
    <flattenmapper/>
  </pathconvert>
  <echo message="files created: ${made}"/>
</target>
$ ant -q glob-without-to
     [echo] files created: null
BUILD SUCCESSFUL
Total time: 0 seconds

And a mapper that matches nothing results in nothing being copied: no file, no directory and no warning:

<target name="no-match" depends="clean">
  <copy todir="${out}/y">
    <fileset dir="data"/>
    <globmapper from="*.txt" to="*.bak"/>
  </copy>
  <available file="${out}/y" type="dir" property="y.exists"/>
  <echo message="destination directory created: ${y.exists}"/>
</target>
$ ant -q no-match
     [echo] destination directory created: ${y.exists}
BUILD SUCCESSFUL
Total time: 0 seconds

The unset property printed literally means the destination directory was not created. When a copy produces nothing, check the pattern of the mapper first (it is case sensitive and matches the whole name), then the include patterns of the fileset. A mistyped type is the only error Ant reports on its own: nosuch is not a legal value for this attribute.

A complete pipeline

Putting the pieces of the diagram together, the CLI module’s resource templates are selected with a filename selector, filtered and renamed in one copy:

<target name="pipeline" depends="clean">
  <copy todir="${out}/classes">
    <fileset dir="src/main/resources">
      <filename name="**/*.in"/>
    </fileset>
    <filterchain>
      <striplinecomments><comment value="#"/></striplinecomments>
      <replacetokens>
        <token key="VERSION" value="1.4.0"/>
        <token key="BUILD_DATE" value="2026-10-16"/>
        <token key="STATIONS" value="data/stations.csv"/>
      </replacetokens>
    </filterchain>
    <globmapper from="*.in" to="*"/>
  </copy>
  <pathconvert property="made" pathsep=", ">
    <fileset dir="${out}/classes"/>
    <globmapper from="${out}${file.separator}classes${file.separator}*" to="*"/>
  </pathconvert>
  <echo message="created: ${made}"/>
  <loadfile srcfile="${out}/classes/banner.txt" property="out.banner"/>
  <echo message="${out.banner}"/>
</target>
$ ant -q pipeline
     [echo] created: banner.txt, release.properties
     [echo] tidepool 1.4.0 (built 2026-10-16)
     [echo] stations file: data/stations.csv
     [echo] engine: ${engine.name}
     [echo] unknown token stays: @MISSING@
BUILD SUCCESSFUL
Total time: 0 seconds

Both templates were selected by the filename selector, read through the filterchain and renamed by the glob mapper (created: lists the two target names). The comment line was removed by striplinecomments; ${engine.name} was not touched because this chain has no expandproperties; and the %VERSION* and %CODENAME* tokens of release.properties.in were copied unchanged, because this chain’s replacetokens uses the default @ delimiters. The same job with an explicit list of output names would need one copy per file.

The regexp type and its implementations

<regexp pattern="..."/> is a reusable regular expression. It may have an id and be referenced with refid, and it is the nested element of linecontainsregexp, the matches condition, replaceregexp and containsregexp. The mappers and the containsregex and replaceregex string filters use the same mechanism for choosing an implementation, which is governed by the ant.regexp.regexpimpl property (and ant.regexp.matcherimpl for the regexp mapper, as the manual’s mapper page notes): the value is the class name org.apache.tools.ant.util.regexp.Jdk14RegexpRegexp, org.apache.tools.ant.util.regexp.JakartaOroRegexp or org.apache.tools.ant.util.regexp.JakartaRegexpRegexp, or your own Regexp implementation. When it is not defined Ant uses the java.util.regex wrapper, which is always available and which the manual calls the best choice now that Jakarta ORO is retired. $ and ^ anchors depend on the line terminator the library recognises (the wrapper uses only \n), so test patterns on Unix and Windows.

The Jakarta implementations need the matching optional JAR (ant-apache-oro.jar or ant-apache-regexp.jar) and the library itself. Without the library Ant says so:

$ ant -q -Dant.regexp.regexpimpl=org.apache.tools.ant.util.regexp.JakartaOroRegexp chain-token
BUILD FAILED
/work/tidepool/build.xml:124: Class org.apache.tools.ant.util.regexp.JakartaOroRegexp could not be loaded because of an invalid dependency.
Total time: 0 seconds

Equivalents in Maven and Gradle

Need Ant Maven Gradle

Replace tokens or properties in copied text files

filterset, replacetokens, expandproperties

Resource filtering: <filtering>true</filtering> on a <resource>, which replaces ${...} and @...@ references (see Resources & Filtering)

processResources with expand(...) or filter(ReplaceTokens...) on a Copy task (see Working with Files)

Transform lines while copying

filterchain readers

none in the core plugins; run Ant from the maven-antrun-plugin

filter(...) accepts the same Ant FilterReader classes, so HeadFilter and LineContains work in a Gradle copy

Rename on copy

a mapper

none for resources

rename and eachFile in a copy specification

Gradle ships Ant’s filter classes, which is why the snippets in its manual import org.apache.tools.ant.filters.ReplaceTokens: the reader you learned here is the one Gradle runs.

Where the books are outdated

Java Development with Ant (Hatcher and Loughran, 2002) was written for Ant 1.5 and covers filtersets, filterchains and mappers. Later releases added tokenfilter (1.6), the chained and composite mappers (1.7.0), sortfilter and firstmatchmapper (1.8.0), cutdirsmapper (1.8.2), the matchAny parameter of linecontains (1.10.4) and native2asciifilter (1.9.8), none of which that edition can describe, and the JavaScript examples of scriptfilter and scriptmapper no longer run on a JDK without an added script engine.

Common mistakes

  • A filterset replaces only the tokens it defines and does not touch ${...}; if the output still contains ${name}, add expandproperties or declare the value as a token.

  • The filter task defines filters that are applied only when filtering="true" is set on the copy.

  • Filtering a binary file (a JAR, an image) corrupts it. Select text files with a filename selector, as in the pipeline above.

  • Unset properties stay as ${name} in output, so a build can succeed with an unexpanded value.

  • A globmapper without to writes files named null; a mapper that matches nothing writes nothing.

  • In a regexpmapper, write $$ for the end-of-line anchor and remember that to is the entire new name.

  • scriptfilter and scriptmapper fail on a JDK that has no script engine.

Where to go next