Scripting in Ant

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.

Ant build files are declarative, and most builds never need more than the tasks in Ant’s own library. When a build needs a small piece of logic that no task provides, Ant can run a script in the middle of a build: inline in a <script> task, as a new task defined with <scriptdef>, or as a condition, a selector, a filter or a mapper written in a scripting language. This page covers all six elements, the two script-engine managers behind them, and the problem every reader meets on a modern JDK: the JavaScript engine that used to ship with Java is gone. It is written against Apache Ant 1.10.18. Every example was run on 2026-10-16 on macOS with Oracle JDK 26 (JBR 21.0.10 and JBR 17.0.14 were also used where stated), with the Ant 1.10.18 distribution unpacked in a scratch directory. Paths in the output were shortened to .. Where something could not be run here it is labelled "not run here".

The elements are in Ant’s own ant.jar (the BSF manager is in ant-apache-bsf.jar), and none of them brings a language with it. The language engine is always an extra JAR that you add, which is why this page spends most of its time on the class path. The related extension mechanisms are macros and presets (Macros & Presets), Java tasks (Writing Custom Tasks) and custom conditions and selectors (Custom Types, Conditions & Selectors). Choose a script when the logic is a few lines, a prototype, or specific to one build file.

The six script elements

Element Where it is used What the script sees

<script>

A task, anywhere a task is allowed.

project, self (the task), and with setbeans the properties, references and targets by name.

<scriptdef>

Defines a new task, then used like any task.

project, self, attributes, elements, and self.text for nested text.

<scriptcondition>

A condition, wherever a condition is allowed (<condition>, <waitfor>).

The same beans as <script>, and self is the condition: self.value or the script’s own boolean result.

<scriptselector>

A selector inside a fileset (see Selectors).

self, filename, file, basedir; call self.setSelected(boolean).

<scriptfilter>

A filter in a <filterchain> or <tokenfilter> (see Filters & Mappers).

self with getToken() and setToken(String); setting a null token drops the line.

<scriptmapper>

A mapper (see Filters & Mappers).

source (the name to map), self.addMappedName(String) and self.clear().

All six take the same core attributes, which are listed below once and then used throughout the page.

Attribute Effect

language

Required. The language name as known to the script engine manager (javascript, groovy, …​).

manager

Since Ant 1.7. bsf, javax or auto (default). bsf uses Apache BSF, javax uses the javax.script (JSR 223) manager that the JDK provides, and auto uses BSF if its classes can be loaded and javax otherwise.

src

A file with the script, instead of nested text. The nested text is then not used.

encoding

The encoding of the src file (Ant 1.10.2). The default is the JVM’s default encoding.

setbeans

Whether to bind every property, reference and target as a variable. true by default for <script>, <scriptcondition>, <scriptselector>, <scriptfilter> and <scriptmapper>. For <scriptdef> it was added in Ant 1.10.13 and the default is false for backward compatibility. With false, only project and self are bound.

classpath, classpathref, nested <classpath>

Where the language engine is found. If one is given, it becomes the context class loader of the script and the class loader given to the manager.

<scriptdef> adds name (the new task’s name), uri (an XML namespace for it), compiled (Ant 1.10.2: compile the script once if the manager is javax and the engine implements javax.script.Compilable), loaderRef (share the class loader between several definitions), nested <attribute name="..." default="..."> (the default since Ant 1.10.13), <element name="..." type="..." classname="...">, and any resource collection that supplies the script text. <scriptcondition> adds value, the default result (false).

A JDK without a JavaScript engine

JavaScript was the language the Ant manual used for years, because Rhino and then Nashorn shipped with Java. Nashorn was deprecated in Java 11 and removed in Java 15 (JEP 372). Since then a <script language="javascript"> fails on a plain JDK, and so does any build file or example from a book written before 2020. A file with the simplest possible script:

hello-js.xml
<project name="js-demo" default="js">
  <target name="js">
    <script language="javascript">
      var greeting = "hello from JavaScript";
      project.log(greeting);
    </script>
  </target>
</project>

Run on JDK 26 with no extra JARs:

$ ant -f hello-js.xml
Buildfile: ./hello-js.xml

js:
Java 15 has removed Nashorn, you must provide an engine for running JavaScript yourself. GraalVM JavaScript currently is the preferred option.

BUILD FAILED
./hello-js.xml:3: Unable to create javax script engine for javascript

Total time: 0 seconds

The first line comes from Ant, which knows what happened. The same two lines appeared with JAVA_HOME set to JBR 21.0.10 and to JBR 17.0.14, and a three-line check program (new ScriptEngineManager().getEngineFactories()) listed zero engines on all three runtimes. The only other JDK installed on this machine was Oracle JDK 26, so no JDK from 11 to 14 (where Nashorn still exists) could be tried here; that part is from the manual. There is no engine of any kind in a plain JDK, so every language needs a JAR.

Note that the error does not name the missing JAR. When a script fails with Unable to create javax script engine for <language>, the language is spelled wrong, or its engine is not on the class path of the script, which is the next section.

Adding an engine

An engine JAR reaches a script in one of three ways, from the widest to the narrowest scope.

  1. ANT_HOME/lib, or ~/.ant/lib: every build of that installation, or of that user.

  2. -lib <dir-or-jar> on the command line: one run (see Running Ant).

  3. A classpath or classpathref attribute (or a nested <classpath>) on the script element: that script only, which keeps the Ant installation free of language JARs and lets the build file say what it needs. The examples below use -lib for brevity and show the classpathref variant once.

Groovy with JSR 223

Groovy implements javax.script, so <script language="groovy"> works with the JSR 223 manager. It needs two JARs from Maven Central, org.apache.groovy:groovy and org.apache.groovy:groovy-jsr223 (the version of both is 6.0.0 here, the current release on Central on 2026-10-16). The groovy-bsf module, which the Library Dependencies table of the 1.10.18 manual still lists for script and scriptdef, is for the older BSF manager and is not needed here. A tiny Maven project fetches them, together with anything they depend on, into a directory (Maven 3.10.0, not part of the build itself):

fetch-groovy/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>demo</groupId><artifactId>fetch-groovy</artifactId><version>1</version>
  <dependencies>
    <dependency><groupId>org.apache.groovy</groupId><artifactId>groovy</artifactId><version>6.0.0</version></dependency>
    <dependency><groupId>org.apache.groovy</groupId><artifactId>groovy-jsr223</artifactId><version>6.0.0</version></dependency>
  </dependencies>
</project>
$ cd fetch-groovy
$ mvn -q dependency:copy-dependencies -DoutputDirectory=../lib-groovy
$ ls -la ../lib-groovy
-rw-r--r--@ 1 albertoirurueta  staff  9361435 Sep 19 03:01 groovy-6.0.0.jar
-rw-r--r--@ 1 albertoirurueta  staff    23337 Sep 19 03:02 groovy-jsr223-6.0.0.jar

(The dependency tasks on Maven Resolver Ant Tasks are the way to do the same inside an Ant build, without the extra Maven project.)

Without the JARs, the failure is the same one Ant reports for JavaScript, but without the Nashorn explanation:

$ ant script
Buildfile: ./build.xml

script:

BUILD FAILED
./build.xml:6: Unable to create javax script engine for groovy

With -lib ../lib-groovy the build file below runs. It is the first of several targets in one build.xml that reads the tidepool station list:

build.xml (script target)
<project name="tidepool-scripting" default="all" basedir=".">
  <property name="stations" value="data/stations.csv"/>

  <target name="script">
    <script language="groovy">
      def rows = new File(project.baseDir, project.getProperty("stations")).readLines().drop(1)
      project.setProperty("station.count", rows.size().toString())
      self.log("loaded " + rows.size() + " stations")
    </script>
    <echo>station.count=${station.count}</echo>
  </target>
  ...
</project>
$ ant -lib ../lib-groovy script
script:
   [script] loaded 3 stations
     [echo] station.count=3

BUILD SUCCESSFUL

self.log prints with the task name, as any task does, and project.setProperty creates a property that later tasks read in the usual way, so a script is a handy way to compute a value. Which setter to call matters, because of Ant’s rule that a property cannot be changed once set (see Properties); the next section shows the difference.

The same -lib switch was repeated with JBR 21.0.10 and JBR 17.0.14 as the runtime and printed the same output, so Groovy 6.0.0 is not tied to JDK 26.

The script can be moved out of the build file with src (the encoding attribute matters only for this form), and the engine can be named by a path in the build file instead of -lib. This build file and a one-line hello.groovy (self.log("script loaded from a file, encoding utf-8")) were run with no -lib at all:

src.xml
<project name="src-demo" default="hello">
  <path id="groovy.path">
    <fileset dir="../lib-groovy" includes="*.jar"/>
  </path>
  <target name="hello">
    <script language="groovy" classpathref="groovy.path" src="hello.groovy" encoding="UTF-8"/>
  </target>
</project>
hello:
   [script] script loaded from a file, encoding utf-8

JavaScript with GraalJS

The manual’s recommendation is the GraalVM JavaScript engine, which ships as ordinary Maven artifacts (no GraalVM installation is needed). Three artifacts, all version 25.4.4.1.1 (current on Central on 2026-10-16), cover it: org.graalvm.polyglot:polyglot, org.graalvm.polyglot:js-community (a POM artifact whose dependencies are the language implementation) and org.graalvm.js:js-scriptengine. Fetched with the same kind of POM as for Groovy, with <type>pom</type> on js-community, they resolved to 13 JARs and 67 MB (the two POMs that the copy step also leaves in the directory were deleted). The directory was called lib-graal, and the failing build file from above was run again:

$ ant -lib ../lib-graal -f hello-js.xml
Buildfile: ./hello-js.xml

js:
WARNING: A restricted method in java.lang.System has been called
...
   [script] [engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to native code.
   [script] Your Java runtime '26+35-2893' is incompatible with optimized Truffle runtime version '25.4.4.1.1'.
   [script] The Java runtime version must be greater or equal to JDK '25' and smaller than JDK '26'.
   ...
hello from JavaScript

BUILD SUCCESSFUL

The script ran. The output above is shortened with ... because on JDK 26 the engine prints about twenty lines of warnings first. They have two causes, both specific to this JDK: Truffle’s optimising runtime accepts JDK 25 only in this release, so it falls back to the interpreter, and the JDK warns about native access and sun.misc.Unsafe use by the engine. They are harmless for build scripts. They can be silenced through ANT_OPTS:

$ export ANT_OPTS="-Dpolyglot.engine.WarnInterpreterOnly=false --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow"

On JBR 21.0.10 and JBR 17.0.14 the same build printed none of these warnings. Always check the warning text of the engine on the JDK you actually use, since the engine version and the JDK version are coupled.

The manual’s own JavaScript example, changed only to give the project a name that is a valid JavaScript identifier, creates echo tasks from the script:

js.xml (squares project, main target)
<project name="squares" default="main" basedir=".">
  <target name="main">
    <script language="javascript"> <![CDATA[
      for (i = 1; i <= 3; i++) {
        echo = squares.createTask("echo");
        echo.setMessage(i*i);
        echo.perform();
      }
    ]]> </script>
  </target>
</project>
main:
     [echo] 1
     [echo] 4
     [echo] 9

The project name matters: with setbeans on, the project is bound under its name, but only when that is a valid identifier for the language. With a project called js-squares the script js_squares.createTask("echo") failed with:

BUILD FAILED
./hy.xml:3: ReferenceError: ReferenceError: "js_squares" is not defined

project is always available and does not depend on the name; prefer it in scripts you want to copy to other builds. The same rule applies to property names: a property such as build.dir is not a valid variable, and so is not bound as one. Read it with project.getProperty("build.dir").

The Nashorn compatibility mode

When the engine is GraalJS, Ant switches on the option polyglot.js.allowAllAccess, so scripts can use Ant’s objects and Java classes. By default it also enables GraalJS’s Nashorn compatibility mode. That mode is narrower than it sounds. The Rhino/Nashorn habit of importClass(java.io.File) is not enabled by it. The two targets used for these checks, in the same js.xml, differ only in the first line:

js.xml (compat and moz targets)
<target name="compat">
  <script language="javascript"> <![CDATA[
    importClass(java.io.File);
    var f = new File("data/stations.csv");
    project.log("size of " + f.getName() + " = " + f.length());
  ]]> </script>
</target>

<target name="moz">
  <script language="javascript"> <![CDATA[
    load("nashorn:mozilla_compat.js");
    importClass(java.io.File);
    var f = new File("data/stations.csv");
    project.log("size of " + f.getName() + " = " + f.length());
  ]]> </script>
</target>
compat:

BUILD FAILED
./js.xml:12: ReferenceError: ReferenceError: "importClass" is not defined

What the compatibility mode does provide, as run here, is the Nashorn compat script loader. With load("nashorn:mozilla_compat.js"); before importClass(java.io.File) the script printed size of stations.csv = 239. The same script with the magic property ant.disable.graal.nashorn.compat set to true (ant -Dant.disable.graal.nashorn.compat=true) failed:

moz:

BUILD FAILED
./js.xml:26: TypeError: TypeError: Cannot load script: nashorn:mozilla_compat.js

The portable way, which works with the compatibility mode on or off, is Java.type:

js.xml (type target)
<script language="javascript"> <![CDATA[
  var File = Java.type('java.io.File');
  var f = new File("data/stations.csv");
  project.log("size of " + f.getName() + " = " + f.length());
]]> </script>
type:
size of stations.csv = 239

The manual offers Java.type and the compatibility script load("nashorn:mozilla_compat.js") as equal options; this page’s recommendation is to write Java.type on a new build and leave the compatibility script out. GraalJS is not a drop-in replacement for Nashorn, and the GraalVM project has a migration guide for the differences.

Other languages

The manual’s examples also show BeanShell, JRuby, Jython, JudoScript, Rexx (NetRexx) and TCL (jacl, jtcl). They were not run here; the language JAR for each is listed in the manual’s "Library Dependencies" table. Rhino JavaScript is the one the manual ties to BSF. Ant’s side of the BSF manager, ant-apache-bsf.jar, is in ANT_HOME/lib already; the BSF JAR itself is not.

BSF and JSR 223

Ant has two ways to talk to a language:

  • JSR 223 (javax.script, since Java 6) is part of the JDK. It looks the engine up by language name, using the ServiceLoader mechanism, so a JAR with a META-INF/services/javax.script.ScriptEngineFactory entry is enough.

  • Apache BSF (Bean Scripting Framework) is an older library with its own engine registry. Ant 1.6 and later need BSF 2.3.0 or later, and 2.4.0 to use Rhino 1.5R4 or later; BSF 2.4.0 also needs commons-logging.

The manager attribute chooses between them. auto, the default, uses BSF when its classes can be loaded (the manual describes it so), and javax otherwise. Practical consequences:

  • Without a BSF JAR, auto is the same as javax. This is the normal situation today: JSR 223 engines exist for the languages used in builds (Groovy and GraalJS were run here), and nothing in this page needed BSF.

  • manager="javax" is the explicit choice when both are installed, which avoids a surprise if a BSF JAR is ever added to ANT_HOME/lib. Running the Groovy build with manager="javax" printed the same lines as the default.

  • manager="bsf" without BSF fails, and the message names the missing class:

script:

BUILD FAILED
./bsf.xml:6: Unable to load script engine manager (org.apache.bsf.BSFManager)

BSF with a working engine was not run here.

The beans bound for a script are the same under both managers.

How a script sees the build

Inside a script, the Ant objects are the ordinary Java API (see Embedding Ant for the same classes used from a Java program):

  • project is the org.apache.tools.ant.Project: getProperty, setProperty, setNewProperty, getBaseDir, log, createTask, createDataType, getReference, getTargets, executeTarget.

  • self is the script element itself: for <script>, the Script task; for <scriptdef>, the ScriptDefBase (with log, fail, text).

  • With setbeans on, each property, reference id and target is bound by its name, so stations in the script above is the property of that name. A name that is not a valid identifier in the language cannot be used as a variable: the project name js-squares above, and a property with dots such as a.b.

The effect was checked with Groovy on properties a.b, a_b and keep (value original):

t:
   [script] a_b = underscore
   [script] a.b not bound as a.b
   [script] script variable keep = changed by assignment
   [script] project.getProperty('keep') = original
   [script] after setProperty = set by setProperty
   [script] after setNewProperty = set by setProperty

So a_b is a variable and a.b is not (read it with project.getProperty("a.b")); assigning to the bound variable keep changes the script’s own variable and leaves the Ant property alone; project.setProperty does overwrite the property (a user property given with -D would not be overwritten); and project.setNewProperty follows Ant’s rule and is ignored once the property exists, as the <property> task is. Use setNewProperty for properties a user may override, and setProperty only to deliberately replace a value. With setbeans="false" only project and self exist:

t:
   [script] setbeans=true : stations=data/stations.csv
   [script] setbeans=false: No such property: stations for class: Script2
   [script] setbeans=false: via project -> data/stations.csv

Prefer project for reading and setting properties, self.log for logging, and createTask and createDataType for tasks and types. The following target, Groovy again, creates and runs three echo tasks from a list, the Groovy counterpart of the manual’s squares example:

build.xml (script-tasks target)
<target name="script-tasks">
  <script language="groovy">
    ["SF", "BOS", "HNL"].each { id ->
      def e = project.createTask("echo")
      e.setMessage("station id " + id)
      e.perform()
    }
  </script>
</target>
script-tasks:
     [echo] station id SF
     [echo] station id BOS
     [echo] station id HNL

The manual’s example uses perform() rather than execute(): perform() is the method Ant itself calls, and it wraps execute() with the build events and the logging. The same applies to any task created by a script.

When a script creates targets itself, the manual asks that each get a distinct location, so that error messages can name them.

scriptdef

<scriptdef> defines a task whose body is a script. It is the next step after a macrodef, when the logic needs real code: parsing, conditionals over several values, or loops, and when a prototype of a Java task is wanted. The tidepool example reads a row from stations.csv and logs one column of it:

build.xml (stationinfo task and scriptdef target)
<scriptdef name="stationinfo" language="groovy">
  <attribute name="id"/>
  <attribute name="column" default="name"/>
  <element name="fileset" type="fileset"/>
  <![CDATA[
    def id = attributes.get("id")
    if (id == null) self.fail("stationinfo needs an id")
    def col = ["id": 0, "name": 1, "latitude": 2, "longitude": 3, "constituents": 4]
    def idx = col[attributes.get("column")]
    if (idx == null) self.fail("unknown column " + attributes.get("column"))
    def line = new File(project.baseDir, project.getProperty("stations")).readLines().drop(1).find { it.startsWith(id + ",") }
    if (line == null) self.fail("no station " + id)
    self.log(id + " " + attributes.get("column") + " = " + line.split(",")[idx])
    def sets = elements.get("fileset")
    self.log("filesets passed: " + (sets == null ? 0 : sets.size()))
  ]]>
</scriptdef>

<target name="scriptdef">
  <stationinfo id="BOS"/>
  <stationinfo id="HNL" column="latitude">
    <fileset dir="src"/>
    <fileset dir="data"/>
  </stationinfo>
</target>
scriptdef:
[stationinfo] BOS name = Boston
[stationinfo] filesets passed: 0
[stationinfo] HNL latitude = 21.3067
[stationinfo] filesets passed: 2

The rules the example uses:

  • attributes is a map from the attribute name to its value. Attribute and element names are lower-cased by Ant, so write get("column") even if the definition says Column. An attribute that was not given and has no default is null, which is why the script checks id. The default attribute exists since Ant 1.10.13; in earlier versions a script had to supply the default itself.

  • elements maps a nested element name to the list of the instances found, or null when there are none, so a loop over a missing element needs the null check shown. An element is declared with type (an Ant type or task name, with the namespace URI and : in front when it has one, for example antlib:example.org:newtype) or with classname.

  • self.fail(message) raises a BuildException. self.text is the nested text of the call, and self.log logs under the new task’s name. This is why the output lines start with [stationinfo].

  • The uri attribute puts the new task into an XML namespace, which callers must then use. The manual declares scripts in a new namespace so that Ant cannot confuse them with a task of the same name; its example defines an echo that way.

  • A script is evaluated at each call. compiled="true" compiles it once when the manager and the engine allow it. Groovy’s engine supports compilation; the same target with compiled="true" printed identical output. Nothing in the output tells compiled and interpreted apart, so measure before assuming a speedup.

Failing is as easy as calling a task with an unknown id:

$ ant -lib ../lib-groovy scriptdef-error
scriptdef-error:

BUILD FAILED
./build.xml:53: javax.script.ScriptException: no station XXX
	at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:469)
	...
Caused by: no station XXX
	at org.apache.tools.ant.taskdefs.optional.script.ScriptDefBase.fail(ScriptDefBase.java:129)

The exception is the engine’s wrapper around the BuildException, and it prints a stack trace, which is long (the rest is omitted above). The line number in the message is the call site, line 53 of build.xml, and not a line in the script. A syntax error is also found only when the script is executed, not when the project is parsed. With a truncated expression in a scriptdef, the target below printed its first task and then failed at the call:

t:
     [echo] before

BUILD FAILED
./syn.xml:5: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Script1.groovy: 2: Missing ')' @ line 2, column 16.

The manual says that the message names the scriptdef for some engines; Groovy’s did not.

<scriptdef> also takes src, classpath and loaderRef, and any resource collection as the script text (since Ant 1.7.1). loaderRef lets several definitions share one class loader, which matters when the language engine is not on the Ant class path.

scriptcondition

<scriptcondition> is a condition (see Conditions & Control Flow) usable inside <condition>, <waitfor> and wherever a nested condition is allowed. It has the default result value and decides in one of two ways: the script’s last value, if it is a boolean (since Ant 1.7.1), or a call to self.setValue(boolean).

build.xml (condition target)
<target name="condition">
  <condition property="many.stations">
    <scriptcondition language="groovy" value="false">
      self.setValue(new File(project.baseDir, project.getProperty("stations")).readLines().size() > 3)
    </scriptcondition>
  </condition>
  <echo>many.stations=${many.stations}</echo>
  <condition property="boolean.result">
    <scriptcondition language="groovy">
      project.getProperty("stations").endsWith(".csv")
    </scriptcondition>
  </condition>
  <echo>boolean.result=${boolean.result}</echo>
</target>
condition:
     [echo] many.stations=true
     [echo] boolean.result=true

(The file has a header and three stations, four lines, hence many.stations is true.) The <condition> task sets the property only when the condition is true, so a false result leaves it unset and ${many.stations} stays literally as written. Details of that pitfall are on the conditions page. setbeans for this element has been available since Ant 1.8.0.

scriptselector

<scriptselector> goes inside a fileset (or any selector container) and is called once for every candidate file. The beans are self, filename (a String), file (a java.io.File) and basedir, besides the usual ones. Only selected can be written, through self.setSelected(boolean).

build.xml (selector target)
<target name="selector">
  <delete dir="out"/>
  <copy todir="out" overwrite="true">
    <fileset dir="src">
      <scriptselector language="groovy">
        self.setSelected(filename.startsWith("b"))
      </scriptselector>
    </fileset>
  </copy>
  <fileset id="sel" dir="out"/>
  <echo>selected: ${toString:sel}</echo>
</target>
selector:
   [delete] Deleting directory ./out
     [copy] Copying 1 file to ./out
     [echo] selected: b.txt

Of the two files in src, a.txt and b.txt, only b.txt was selected. A script selector costs one script evaluation per file, so for large trees prefer the built-in selectors (Selectors) and use a script only for the rule that none of them can express. <scriptselector> has existed since Ant 1.7.

scriptfilter

<scriptfilter> works on a token at a time: inside <tokenfilter>, a token is a line by default. The script gets self, with getToken() and setToken(String). Setting the token to null deletes it from the output. It can also stand directly in a <filterchain>, where it sees the content in the way the chain splits it. The next target upper-cases the lines of two text files and drops any line that contains bad:

build.xml (filter target) and src/a.txt, src/b.txt
<target name="filter">
  <delete dir="out"/>
  <copy todir="out">
    <fileset dir="src" includes="**/*.txt"/>
    <filterchain>
      <tokenfilter>
        <scriptfilter language="groovy">
          if (self.getToken().contains("bad")) self.setToken(null)
          else self.setToken(self.getToken().toUpperCase())
        </scriptfilter>
      </tokenfilter>
    </filterchain>
  </copy>
  <concat><fileset dir="out"/></concat>
</target>

a.txt holds hello, this is bad and world; b.txt holds second file.

filter:
     [copy] Copying 2 files to ./out
   [concat] HELLO
   [concat] WORLD
   [concat] SECOND FILE

The class TokenFilter.Filter is the Java way to write the same filter; the filterchain page shows custom string filters and tokenizers.

scriptmapper

<scriptmapper> maps a source name to any number of target names. It is called once for every name, with source bound to the name, and the mapped names are collected with self.addMappedName(String). The list is reset after every call (self.clear() does it by hand). It has no type attribute for <mapper>: it is used as a nested element, wherever a mapper is expected.

The element is used here inside <pathconvert>, which shows all mapped names. Under <pathconvert>, source was the absolute path of each file (found out by printing it), which is why the script takes the file’s name with new File(source).name:

build.xml (mapper target)
<target name="mapper">
  <fileset id="srcfiles" dir="src"/>
  <pathconvert property="mapped.names" refid="srcfiles" pathsep=", ">
    <scriptmapper language="groovy">
      self.addMappedName(new File(source).name.replace(".txt", ".orig"))
      self.addMappedName("copy-of-" + new File(source).name)
    </scriptmapper>
  </pathconvert>
  <echo>${mapped.names}</echo>
</target>
mapper:
     [echo] a.orig, copy-of-a.txt, b.orig, copy-of-b.txt

Two names per source file. Whether a task uses all of them is the task’s decision. <copy> uses the first one by default; with enablemultiplemappings="true" it uses all of them. The same <scriptmapper> inside <copy todir="out">, listing the result with ${toString:result}:

$ ant -Dmulti=false ...
     [copy] Copying 2 files to ./out
     [echo] a.orig;b.orig

$ ant -Dmulti=true ...
     [copy] Copying 2 files to ./out
     [copy] Copied 1 empty directory to 1 empty directory under ./out
     [echo] a.orig;b.orig;copy-of-a.txt;copy-of-b.txt

(The extra "empty directory" line is copy reporting the src directory itself, which the fileset also matches.) The same attribute exists on mappedresources, described on Resource Collections.

Choosing a language

Language Strength Cost

Groovy (JSR 223)

Close to Java syntax, one 9 MB engine, run here on JDK 17, 21 and 26, full access to Java and Ant classes. See Integrating Groovy into Java Applications.

A new language in the build file for readers who know only Java.

JavaScript (GraalJS)

The language of the manual’s examples; widely known.

67 MB of JARs, engine version tied to the JDK version (warnings on JDK 26), not a drop-in for Nashorn.

JRuby, Jython, BeanShell, others

Existing code or team preference. Listed in the manual.

Not run here. Check that the engine supports the Java release you use.

In general: if the logic is longer than about twenty lines, or is used by more than one build, write a Java task (the next step is Writing Custom Tasks) so that it can be compiled, tested with AntUnit, and used from IDEs.

Security and maintenance

  • A script runs with the full permissions of the JVM and can read files, run programs and open network connections. Inline scripts in a build file you did not write deserve the same review as a Java task, and a build file that takes a src path from a property is only as safe as that property. Scripts in untrusted builds should not be run at all.

  • The engine JAR is part of the supply chain. Pin its version, take it from Maven Central and verify it as for any dependency (see Supply Chain Security).

  • The engine can change under the build. A Java upgrade can break a script that worked (as Java 15 did for every JavaScript script), so keep the engine JAR next to the build file (a lib directory used with -lib, or a classpathref) and not in a shared ANT_HOME/lib.

  • In the other tools: Gradle build scripts are programs in Groovy or Kotlin, so the question hardly arises there (see Ant Integration for calling Ant from Gradle); Maven can run Ant tasks, scripts included, through the maven-antrun-plugin.

Where the books and older build files are outdated

  • Every JavaScript example in material from before 2020 assumes Nashorn or Rhino. It needs GraalJS (or another engine) on the class path today and, for Rhino idioms, rewriting with Java.type.

  • importPackage and importClass are not defined by the default Nashorn compatibility mode of GraalJS, as shown above.

  • Examples calling <script language="beanshell"> or other languages never say which JAR is required; the library table of the manual does.

  • <scriptdef> before Ant 1.10.13 had no default on <attribute> and no setbeans.

References