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.
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 |
|---|---|---|
|
String that opens a token. |
|
|
String that closes a token. |
|
|
One properties file whose entries are tokens and values. |
none |
|
Whether the replacement text is searched for more tokens (Ant 1.6.3). |
|
|
What to do when a |
|
|
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.
Source: Apache Ant manual: FilterSet
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.
Source: Apache Ant manual: FilterSet
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.
Source: Apache Ant manual: FilterSet
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.
Source: Apache Ant manual: Filter
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.
Source: Apache Ant manual: FilterReader
The built-in readers
| Reader | What it does and its parameters | Since |
|---|---|---|
|
Replaces |
1.8.3 for the propertyset |
|
Replaces |
1.8.0 for |
|
First |
|
|
Last |
|
|
Keeps lines that contain every nested |
1.7 |
|
Keeps lines matching a regular expression, given as a nested |
1.7 |
|
Attaches |
|
|
Attaches |
1.8.0 |
|
Removes lines that begin with one of the nested |
|
|
Removes Java style comments, both |
|
|
Removes the characters in |
|
|
Replaces each tab with |
|
|
Deletes the characters in |
1.6 |
|
Converts every non US-ASCII character to a backslash-u escape. |
1.6 |
|
Prepends the content of the |
1.6 |
|
Reads all lines and sorts them; |
1.8.0 |
|
Splits the input into tokens and passes them through string filters; described below. |
1.6 |
|
Prints the basic constants of a Java class file as |
|
|
The |
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 #.
Source: Apache Ant manual: ReplaceTokens
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.
Source: Apache Ant manual: HeadFilter
Source: Apache Ant manual: LineContains
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.
Source: Apache Ant manual: EscapeUnicode
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.
Source: Apache Ant manual: SortFilter
Source: Apache Ant manual: UniqFilter
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.
Source: Apache Ant manual: TokenFilter
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.
Source: Apache Ant manual: ScriptFilter
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 |
|---|---|
|
One of the built-in implementation names below. Exactly one of |
|
Class name of an implementation, with |
|
Arguments for the implementation. |
|
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
Source: Apache Ant manual: Mapper and PathConvert
The mappers one by one
| Mapper | from and to |
Behaviour | Since |
|---|---|---|---|
|
ignored |
The target name equals the source name. |
|
|
ignored |
The target name is the source name without its directories. |
|
|
|
Every source maps to the same target name. Used, for example, with |
|
|
both, one |
The text matched by |
|
|
both, regular expressions |
Back-references |
|
|
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. |
|
|
both, glob syntax |
The inverse of |
1.6.0 |
|
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 |
|
none, nested mappers |
Pipes the name through the nested mappers: the results of one are the inputs of the next. |
1.7.0 |
|
none, nested filter readers |
Applies a filterchain to the name. |
1.6.3 |
|
|
Runs a script once per source; |
1.7 |
|
none, nested mappers |
Returns the result of the first nested mapper that matches, unlike |
1.8.0 |
|
|
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.
Source: Apache Ant manual: glob mapper
Source: Apache Ant manual: regexp mapper
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.
Source: Apache Ant manual: Mapper, glob
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.
Source: Apache Ant manual: Copy, FilterChains and Mapper
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
Source: Apache Ant manual: Regexp
Equivalents in Maven and Gradle
| Need | Ant | Maven | Gradle |
|---|---|---|---|
Replace tokens or properties in copied text files |
|
Resource filtering: |
|
Transform lines while copying |
|
none in the core plugins; run Ant from the |
|
Rename on copy |
a mapper |
none for resources |
|
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.
Source: Gradle: Filtering files
Source: Gradle: Renaming files
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}, addexpandpropertiesor declare the value as a token. -
The
filtertask defines filters that are applied only whenfiltering="true"is set on the copy. -
Filtering a binary file (a JAR, an image) corrupts it. Select text files with a
filenameselector, as in the pipeline above. -
Unset properties stay as
${name}in output, so a build can succeed with an unexpanded value. -
A
globmapperwithouttowrites files namednull; a mapper that matches nothing writes nothing. -
In a
regexpmapper, write$$for the end-of-line anchor and remember thattois the entire new name. -
scriptfilterandscriptmapperfail on a JDK that has no script engine.
Where to go next
-
File Operations for the tasks that take these filters and mappers.
-
Conditions & Control Flow for
matches,uptodatewith a mapper and the other conditions. -
Custom Types, Conditions & Selectors for writing your own reader or mapper.
-
Properties for the
loadpropertiesandpropertysettypes used with filters.