File Operations

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, and every example was run with it on JDK 26 on macOS on 2026-10-16. All the examples are small build files for the tidepool scenario used across this section, and each one is a complete <project> that runs with ant -f <file> <target> from the project directory. The console output shown is the real output, with three kinds of lines left out: the Buildfile: line, blank lines and the Total time: line. The project directory is shortened to /work/tidepool in paths.

The file tasks are the part of Ant that a build uses most after compiling and packaging: they stage sources, filter resources, assemble a distribution directory, clean up and fix files up before they are archived. They are small and independent, which is why they are listed here task by task. The datatypes they rely on — filesets, patternsets, selectors, mappers and filter chains — are described on Paths, Filesets & Patterns, Selectors, Resource Collections and Filters & Mappers. Packaging the staged files into JARs, ZIPs and TARs is on Packaging Archives.

The file tasks at a glance

Task What it does Since or status

copy

copies a file, a directory tree or any file-system resource collection; optional token filtering, filter chains, re-encoding and name mapping

core task; force 1.8.2, quiet 1.8.3

move

moves or renames a file, a directory or a collection of files; same options as copy

renaming a directory with file since 1.6.3

delete

deletes a file, a link, a directory tree or a collection of files

link attribute 1.10.16

mkdir

creates a directory and any missing parents; does nothing if it exists

core task

sync

makes a directory an exact mirror of one or more resource collections, deleting what is not in them

1.6; preserveInTarget 1.7

touch

creates a file or sets its modification time

now value 1.8, mkdirs 1.6.3

concat

concatenates resources into a file or to the console

usable as a resource collection since 1.7.1; filterbeforeconcat 1.10.10

fixcrlf

normalises line endings, tabs and the end-of-file character

usable in a filter chain since 1.7

replace, replaceregexp

replace a string or a regular expression inside text files

replaceregexp failonerror 1.10.15

tempfile

sets a property to the name of a file that does not exist yet

createfile 1.8

truncate

sets or adjusts the length of files

1.7.1

basename, dirname, length, makeurl

set a property from the name, the directory, the size or the URL of a file

length 1.6.3

chmod, setpermissions

change file permissions, chmod through the Unix executable and setpermissions through Java NIO

setpermissions 1.10.0

symlink, mklink

create and manage symbolic links; mklink also hard links and junctions on Windows

mklink 1.10.16

copydir, copyfile, deltree, rename

older equivalents of copy, delete and move

deprecated, still present in 1.10.18

Three rules apply to every task on this page. First, relative paths are resolved against the project’s basedir, not against the directory Ant was started in (the one exception is the link attribute of symlink, described below). Second, a task that takes a nested fileset or another directory-based element obeys the default exclusions described on Paths, Filesets & Patterns. Third, a task does nothing when its work is already done: copy skips files that are not newer than their destination, and replace, replaceregexp and fixcrlf write a file only when its content changes. That last behaviour keeps timestamps stable, so a task that ran but changed nothing does not trigger a rebuild further down the chain.

Copying files with copy

copy copies a single file (file), a directory tree or any set of file-system resources. A nested resource collection needs the destination to be a directory (todir); the single-file form takes either tofile (a new name) or todir (the same name in another directory). By default a file is copied only if the source is newer than the destination or the destination does not exist.

Attribute Meaning Default

file

the file to copy; required unless a nested resource collection is used

none

tofile, todir

the destination file or directory; with several sources only todir is allowed

none (one is required)

overwrite

copy even when the destination is newer

false

force

overwrite read-only destination files (since 1.8.2)

false

preservelastmodified

give the copy the modification time of the source

false

filtering

apply the project-wide <filter> tokens

false

flatten

ignore the directory structure of the sources and put every file straight into todir

false

includeEmptyDirs

copy the empty directories found in nested filesets

true

failonerror

stop the build when a source is missing or the copy fails; if false, log a warning instead

true

quiet

with failonerror="false", do not even log the warning (since 1.8.3)

false

verbose

log every file copied

false

encoding, outputencoding

character set to read and to write when filtering (outputencoding since 1.6; it defaults to encoding when that is set)

the JVM default character set

enablemultiplemappings

with a mapper, process every mapping of a source instead of only the first (since 1.6)

false

granularity

leeway in milliseconds before a file counts as out of date (since 1.6.2)

1000, or 2000 on DOS-style file systems

The nested elements are any file-system resource collection (fileset, filelist, dirset, path, and so on), a mapper (the identity mapper by default), any number of filterset elements and any number of filterchain elements. When both filter chains and filter sets are present, all the chains run first.

Copying files and trees

The first example copies a resource directory, then a single file twice — into a directory and under a new name — and runs the copy a second time to show that nothing is copied when nothing changed. The resource directory contains a notes.txt~ file, an editor backup that the default excludes keep out of the fileset.

copy-basic.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="copy-basic" default="copy-demo" basedir=".">
  <target name="clean">
    <delete dir="build"/>
  </target>

  <target name="copy-demo">
    <!-- a directory tree: the default excludes skip notes.txt~ -->
    <copy todir="build/classes">
      <fileset dir="src/main/resources"/>
    </copy>
    <!-- one file into a directory, then to a new name -->
    <copy file="data/stations.csv" todir="build/data"/>
    <copy file="data/stations.csv" tofile="build/data/stations-copy.csv"/>
  </target>
</project>
ant -f copy-basic.xml clean copy-demo
ant -f copy-basic.xml copy-demo

The first run copies three files from the resource tree (the two message bundles and logo.png, but not the backup file), then two single files. The second run prints no [copy] lines at all.

clean:
copy-demo:
     [copy] Copying 3 files to /work/tidepool/build/classes
     [copy] Copying 1 file to /work/tidepool/build/data
     [copy] Copying 1 file to /work/tidepool/build/data
BUILD SUCCESSFUL
copy-demo:
BUILD SUCCESSFUL

Filtering tokens while copying

Ant has two ways to replace tokens in a text file during a copy. A nested <filterset> lists tokens and replacements for that one copy. The token is written between two delimiters, @ on both sides by default, so <filter token="VERSION" value="2.0.0"/> replaces every @VERSION@. The delimiters can be changed with the begintoken and endtoken attributes of the filter set. A filter set can also be defined once with an id, referenced with refid, and loaded from a properties file with a nested filtersfile; in either case, the files are processed in text mode, line by line. The second way is the project-wide <filter> task, whose tokens apply to every <copy> that sets filtering="true". Nested filter sets always apply, whatever the filtering attribute says.

A nested <filterchain> is the more general tool: it runs a sequence of line-oriented filters (expandproperties, replacestring, tokenfilter, linecontains, and others) and is described on Filters & Mappers.

The next build filters a properties template, re-encodes a Latin-1 text file to UTF-8, and then copies a small binary file twice — once untouched and once through a filter set — and prints the sizes with length, a task described further down. The template and the Latin-1 file are the ones used throughout this page.

config/tidepool.properties.in
station.file=@DATA@
version=@VERSION@
city=@CITY@
copy-filter.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="copy-filter" default="filter" basedir=".">
  <property name="version" value="1.0.0"/>

  <target name="filter">
    <delete dir="build/filtered"/>
    <!-- text file: tokens delimited by @ are replaced -->
    <copy file="config/tidepool.properties.in" tofile="build/filtered/tidepool.properties">
      <filterset>
        <filter token="VERSION" value="${version}"/>
        <filter token="DATA" value="data/stations.csv"/>
        <filter token="CITY" value="Brest"/>
      </filterset>
    </copy>
    <!-- re-encode a text file while copying -->
    <copy file="config/legacy-latin1.txt" tofile="build/filtered/legacy-utf8.txt"
          encoding="ISO-8859-1" outputencoding="UTF-8">
      <filterset begintoken="@" endtoken="@"/>
    </copy>
    <!-- never filter binary files -->
    <copy file="src/main/resources/logo.png" tofile="build/filtered/logo-copied.png"/>
    <copy file="src/main/resources/logo.png" tofile="build/filtered/logo-filtered.png">
      <filterset>
        <filter token="VERSION" value="${version}"/>
      </filterset>
    </copy>
    <length file="src/main/resources/logo.png" property="len.src"/>
    <length file="build/filtered/logo-copied.png" property="len.copied"/>
    <length file="build/filtered/logo-filtered.png" property="len.filtered"/>
    <echo message="source=${len.src} copied=${len.copied} filtered=${len.filtered}"/>
    <loadfile srcfile="build/filtered/tidepool.properties" property="props.out"/>
    <echo message="${props.out}"/>
  </target>
</project>
ant -f copy-filter.xml
filter:
     [copy] Copying 1 file to /work/tidepool/build/filtered
     [copy] Copying 1 file to /work/tidepool/build/filtered
     [copy] Copying 1 file to /work/tidepool/build/filtered
     [copy] Copying 1 file to /work/tidepool/build/filtered
     [echo] source=20 copied=20 filtered=22
     [echo] station.file=data/stations.csv
     [echo] version=1.0.0
     [echo] city=Brest
BUILD SUCCESSFUL

The properties file came out with its three tokens replaced. The Latin-1 file is now valid UTF-8, and the byte that was é in Latin-1 (e9) has become the two bytes c3 a9:

od -An -tx1 build/filtered/legacy-utf8.txt
           43  61  66  c3  a9  20  64  65  20  6c  61  20  70  6c  61  67
           65  0a

The last lines of the build output show the classic trap. The 20-byte binary file is 22 bytes after the filtered copy: a filtering copy reads the file as text in the default character set and writes it back, so bytes that are not valid text are replaced and the file is corrupted. The Ant manual warns about this: limit filtering to text files, and copy binary files in a separate copy without any filter, or keep them out of the filtered fileset with an exclude pattern.

A project-wide filter has the opposite weakness: it is global state. Tokens set with <filter> apply to every filtering="true" copy that runs afterwards in the same build, including copies in imported build files.

global-filter.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="global-filter" default="copy-with-global-filter" basedir=".">
  <target name="copy-with-global-filter">
    <!-- a project-wide filter: applies to every copy that sets filtering="true" -->
    <filter token="CITY" value="Brest"/>
    <copy file="config/tidepool.properties.in" tofile="build/global.properties" filtering="true"/>
    <concat><fileset file="build/global.properties"/></concat>
  </target>
</project>

Only the @CITY@ token is replaced; the other two tokens have no filter and stay as they are:

copy-with-global-filter:
     [copy] Copying 1 file to /work/tidepool/build
   [concat] station.file=@DATA@
   [concat] version=@VERSION@
   [concat] city=Brest
BUILD SUCCESSFUL

Mappers, flattening, timestamps and error handling

A nested mapper changes the name of each file on its way to the destination. The name handed to the mapper is relative to the fileset’s directory, so * in a glob mapper matches the path inside the tree. flatten="true" is a shortcut for the flatten mapper. preservelastmodified gives the copy the source’s modification time; without it the copy gets the current time, which makes it newer than its source.

The example copies two message bundles flat, copies them again with a .bak suffix, sets a known time on a file with touch and shows that only the copy made with preservelastmodified kept it, using a date selector. It finishes with the two ways of coping with a missing source file.

copy-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="copy-mapper" default="demo" basedir=".">
  <target name="demo">
    <delete dir="build/mapped"/>
    <!-- 1. flatten: ignore the directory structure -->
    <copy todir="build/mapped/flat" flatten="true">
      <fileset dir="src/main/resources" includes="**/*.properties"/>
    </copy>
    <!-- 2. mapper: rename on the fly -->
    <copy todir="build/mapped/bak">
      <fileset dir="src/main/resources" includes="**/*.properties"/>
      <globmapper from="*" to="*.bak"/>
    </copy>
    <!-- 3. preservelastmodified -->
    <touch file="data/stations.csv" datetime="01/02/2026 10:00 AM"/>
    <copy file="data/stations.csv" tofile="build/mapped/plain.csv"/>
    <copy file="data/stations.csv" tofile="build/mapped/kept.csv" preservelastmodified="true"/>
    <fileset id="kept" dir="build/mapped" includes="*.csv">
      <date datetime="01/02/2026 10:00 AM" when="equal" granularity="60000"/>
    </fileset>
    <pathconvert property="kept.names" refid="kept" pathsep=" ">
      <mapper type="flatten"/>
    </pathconvert>
    <echo message="files that kept the source timestamp: ${kept.names}"/>
    <!-- 4. failonerror=false -->
    <copy file="data/missing.csv" todir="build/mapped" failonerror="false"/>
    <copy file="data/missing.csv" todir="build/mapped" failonerror="false" quiet="true"/>
    <!-- 5. missing source with the default failonerror -->
    <copy file="data/missing.csv" todir="build/mapped"/>
  </target>
</project>
demo:
     [copy] Copying 2 files to /work/tidepool/build/mapped/flat
     [copy] Copying 2 files to /work/tidepool/build/mapped/bak
     [copy] Copying 1 file to /work/tidepool/build/mapped
     [copy] Copying 1 file to /work/tidepool/build/mapped
     [echo] files that kept the source timestamp: kept.csv
     [copy] Warning: Could not find file /work/tidepool/data/missing.csv to copy.
BUILD FAILED
/work/tidepool/copy-mapper.xml:29: Warning: Could not find file /work/tidepool/data/missing.csv to copy.

The first failonerror="false" copy logged the missing file as a warning. The second, with quiet="true", logged nothing. The last copy kept the default and failed the build.

The newer-wins rule and its trap

Because a plain copy skips a destination that is newer than the source, a generated file that someone edited by hand survives the next copy. That is the right behaviour for incremental builds and the wrong behaviour when a build must reproduce a directory exactly. In the example below the source is given an old timestamp, the destination is edited, and a plain copy leaves the edit in place. overwrite="true" replaces it.

pitfalls.xml (target newer)
<?xml version="1.0" encoding="UTF-8"?>
<project name="pitfalls" default="newer" basedir=".">
  <target name="newer">
    <delete dir="build/p"/>
    <mkdir dir="build/p"/>
    <echo file="build/p/src.txt">version one</echo>
    <copy file="build/p/src.txt" tofile="build/p/dst.txt"/>
    <sleep seconds="2"/>
    <!-- the destination is edited by hand, so it is newer than the source -->
    <echo file="build/p/dst.txt">hand-edited, newer</echo>
    <echo file="build/p/src.txt">version two</echo>
    <touch file="build/p/src.txt" datetime="01/01/2020 01:00 AM"/>
    <copy file="build/p/src.txt" tofile="build/p/dst.txt"/>
    <loadfile srcfile="build/p/dst.txt" property="after.plain"/>
    <echo message="after plain copy:     ${after.plain}"/>
    <copy file="build/p/src.txt" tofile="build/p/dst.txt" overwrite="true"/>
    <loadfile srcfile="build/p/dst.txt" property="after.overwrite"/>
    <echo message="after overwrite=true: ${after.overwrite}"/>
  </target>
  <target name="defaultexcludes">
    <delete dir="build/d"/>
    <mkdir dir="build/d/.git"/>
    <mkdir dir="build/d/src"/>
    <touch file="build/d/.git/HEAD"/>
    <touch file="build/d/src/Main.java"/>
    <touch file="build/d/src/Main.java~"/>
    <delete includeemptydirs="true">
      <fileset dir="build/d"/>
    </delete>
    <pathconvert property="left" pathsep=", ">
      <fileset dir="build/d" defaultexcludes="false"/>
      <mapper type="regexp" from="^.*/build/d/(.*)$$" to="\1"/>
    </pathconvert>
    <echo message="left after fileset delete: ${left}"/>
    <delete dir="build/d"/>
    <available file="build/d" type="dir" property="d.exists"/>
    <echo message="after delete dir=: build/d exists = ${d.exists}"/>
  </target>
</project>
newer:
    [mkdir] Created dir: /work/tidepool/build/p
     [copy] Copying 1 file to /work/tidepool/build/p
     [echo] after plain copy:     hand-edited, newer
     [copy] Copying 1 file to /work/tidepool/build/p
     [echo] after overwrite=true: version two
BUILD SUCCESSFUL

Two refinements explain most surprises. The comparison uses granularity: one second by default, because some file systems record time with a coarser resolution than others, and a copy between file systems with different clocks may need a larger value. And a nested <path> or file resource yields absolute names, so a mapper that expects relative names, or no mapper at all, may place files somewhere unexpected; use flatten or a fileset with a dir in that case.

Moving files with move

move takes the same attributes and nested elements as copy, with a few differences that matter. It renames a file or moves a collection of files; since Ant 1.6.3 the file attribute can also rename a whole directory. It overwrites by default (overwrite is true, where copy defaults to false), its granularity default is 0 milliseconds, and it has one extra attribute, performGCOnFailedDelete, which makes Ant run the garbage collector and retry once when it cannot delete a source file (the default is true on Windows, where open file handles are common, and false elsewhere). Internally a move tries a rename first and falls back to copy-then-delete, which is why filter sets and filter chains also work with it.

Deleting files with delete

delete removes a file (file), a link (link, since 1.10.16), a directory with everything in it (dir) or the files of nested resource collections.

Attribute Meaning Default

file, dir, link

what to delete; dir removes the directory and all its contents, link removes a symbolic link or Windows junction without touching its target

none (one is required unless there is a nested collection)

includeemptydirs

with nested filesets, also delete directories left empty

false

quiet

do not report a missing file or a file that cannot be deleted; implies failonerror="false", like rm -f

false

failonerror

stop the build when something cannot be deleted

true

verbose

log every deleted file

false

deleteonexit

if deletion fails, ask the JVM to try again when it exits (since 1.6.2)

false

removeNotFollowedSymlinks

remove links that were not followed because followSymlinks was off (since 1.8.0; also junctions since 1.10.16)

false

performGCOnFailedDelete

run the garbage collector and retry once after a failed delete (since 1.8.3)

platform dependent

The attributes includes, includesfile, excludes, excludesfile and defaultexcludes are deprecated on delete itself: use a nested fileset instead.

The example moves files, mirrors a directory with sync (described next), then deletes with a fileset, with a directory and with a file that does not exist. The tree macro prints the contents of a directory; macros are explained on Macros & Presets.

move-delete-sync.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="move-delete-sync" default="demo" basedir=".">
  <macrodef name="tree">
    <attribute name="dir"/>
    <sequential>
      <pathconvert property="tree.@{dir}" pathsep="${line.separator}  ">
        <fileset dir="@{dir}" includes="**"/>
        <mapper type="regexp" from="^.*/@{dir}/(.*)$$" to="\1"/>
      </pathconvert>
      <echo message="@{dir}:${line.separator}  ${tree.@{dir}}"/>
    </sequential>
  </macrodef>

  <target name="demo">
    <delete dir="build/ops"/>
    <mkdir dir="build/ops/staging/reports"/>
    <mkdir dir="build/ops/site"/>

    <!-- move: rename a file, move a directory, and move a fileset -->
    <copy todir="build/ops/staging" file="data/stations.csv"/>
    <move file="build/ops/staging/stations.csv" tofile="build/ops/staging/stations-2026.csv"/>
    <touch file="build/ops/staging/reports/a.txt"/>
    <touch file="build/ops/staging/reports/b.log"/>
    <move todir="build/ops/archive">
      <fileset dir="build/ops/staging" includes="**/*.txt,**/*.log"/>
    </move>
    <tree dir="build/ops"/>

    <!-- sync: site becomes an exact mirror of generated -->
    <mkdir dir="build/ops/generated"/>
    <touch file="build/ops/generated/index.html"/>
    <touch file="build/ops/site/index.html"/>
    <touch file="build/ops/site/stale.html"/>
    <touch file="build/ops/site/keep.me"/>
    <sync todir="build/ops/site">
      <fileset dir="build/ops/generated"/>
      <preserveintarget>
        <include name="keep.me"/>
      </preserveintarget>
    </sync>
    <tree dir="build/ops/site"/>

    <!-- delete: fileset form, empty directories, and the dir form -->
    <delete includeemptydirs="true" verbose="true">
      <fileset dir="build/ops/archive" includes="**/*.log"/>
    </delete>
    <delete dir="build/ops/staging"/>
    <delete file="build/ops/not-there.txt"/>
    <delete file="build/ops/not-there.txt" verbose="true"/>
  </target>
</project>
demo:
    [mkdir] Created dir: /work/tidepool/build/ops/staging/reports
    [mkdir] Created dir: /work/tidepool/build/ops/site
     [copy] Copying 1 file to /work/tidepool/build/ops/staging
     [move] Moving 1 file to /work/tidepool/build/ops/staging
    [touch] Creating /work/tidepool/build/ops/staging/reports/a.txt
    [touch] Creating /work/tidepool/build/ops/staging/reports/b.log
     [move] Moving 2 files to /work/tidepool/build/ops/archive
     [echo] build/ops:
     [echo]   archive/reports/a.txt
     [echo]   archive/reports/b.log
     [echo]   staging/stations-2026.csv
    [mkdir] Created dir: /work/tidepool/build/ops/generated
    [touch] Creating /work/tidepool/build/ops/generated/index.html
    [touch] Creating /work/tidepool/build/ops/site/index.html
    [touch] Creating /work/tidepool/build/ops/site/stale.html
    [touch] Creating /work/tidepool/build/ops/site/keep.me
     [sync] Removed 1 dangling file from /work/tidepool/build/ops/site
     [echo] build/ops/site:
     [echo]   index.html
     [echo]   keep.me
   [delete] Deleting /work/tidepool/build/ops/archive/reports/b.log
   [delete] Deleting directory /work/tidepool/build/ops/staging
   [delete] Could not find file /work/tidepool/build/ops/not-there.txt to delete.
BUILD SUCCESSFUL

The output does not mention the first not-there.txt at all: deleting a file that is absent is silent at the default log level, while the same call with verbose="true" logs Could not find file ... to delete..

The dir form is not a fileset

For historical reasons <delete dir="x"/> and <delete><fileset dir="x"/></delete> are different operations. The first removes x itself and everything inside it, ignores the default excludes and follows symbolic links. The second removes only the files the fileset selects, honours the default excludes and, unless includeemptydirs="true" is set, leaves the directories. In the build below build/d holds a .git directory and an editor backup Main.java~; both are in Ant’s default excludes, so the fileset delete leaves them behind, and only <delete dir> really empties the tree.

pitfalls.xml (target defaultexcludes)
<?xml version="1.0" encoding="UTF-8"?>
<project name="pitfalls" default="newer" basedir=".">
  <target name="newer">
    <delete dir="build/p"/>
    <mkdir dir="build/p"/>
    <echo file="build/p/src.txt">version one</echo>
    <copy file="build/p/src.txt" tofile="build/p/dst.txt"/>
    <sleep seconds="2"/>
    <!-- the destination is edited by hand, so it is newer than the source -->
    <echo file="build/p/dst.txt">hand-edited, newer</echo>
    <echo file="build/p/src.txt">version two</echo>
    <touch file="build/p/src.txt" datetime="01/01/2020 01:00 AM"/>
    <copy file="build/p/src.txt" tofile="build/p/dst.txt"/>
    <loadfile srcfile="build/p/dst.txt" property="after.plain"/>
    <echo message="after plain copy:     ${after.plain}"/>
    <copy file="build/p/src.txt" tofile="build/p/dst.txt" overwrite="true"/>
    <loadfile srcfile="build/p/dst.txt" property="after.overwrite"/>
    <echo message="after overwrite=true: ${after.overwrite}"/>
  </target>
  <target name="defaultexcludes">
    <delete dir="build/d"/>
    <mkdir dir="build/d/.git"/>
    <mkdir dir="build/d/src"/>
    <touch file="build/d/.git/HEAD"/>
    <touch file="build/d/src/Main.java"/>
    <touch file="build/d/src/Main.java~"/>
    <delete includeemptydirs="true">
      <fileset dir="build/d"/>
    </delete>
    <pathconvert property="left" pathsep=", ">
      <fileset dir="build/d" defaultexcludes="false"/>
      <mapper type="regexp" from="^.*/build/d/(.*)$$" to="\1"/>
    </pathconvert>
    <echo message="left after fileset delete: ${left}"/>
    <delete dir="build/d"/>
    <available file="build/d" type="dir" property="d.exists"/>
    <echo message="after delete dir=: build/d exists = ${d.exists}"/>
  </target>
</project>
defaultexcludes:
    [mkdir] Created dir: /work/tidepool/build/d/.git
    [mkdir] Created dir: /work/tidepool/build/d/src
    [touch] Creating /work/tidepool/build/d/.git/HEAD
    [touch] Creating /work/tidepool/build/d/src/Main.java
    [touch] Creating /work/tidepool/build/d/src/Main.java~
     [echo] left after fileset delete: .git/HEAD, src/Main.java~
   [delete] Deleting directory /work/tidepool/build/d
     [echo] after delete dir=: build/d exists = ${d.exists}
BUILD SUCCESSFUL

Main.java was deleted, but .git/HEAD and src/Main.java~ are still there. To make a fileset delete them, set defaultexcludes="false" on that fileset. The last delete, <delete dir>, removes the lot: the available test then finds no directory, so ${d.exists} stays unset and is printed literally. The example also hints at the most dangerous mistake with this task: <delete dir="${basedir}"/>, or a dir property that is empty and resolves to ., removes the entire project. Keep generated output under a dedicated directory such as build, and delete only that.

Creating directories with mkdir

mkdir has one attribute, dir. It creates the directory and any missing parent directories, and does nothing if the directory exists. It fails when a file with that name already exists:

mkdir-on-file:
    [touch] Creating /work/tidepool/build/afile
BUILD FAILED
/work/tidepool/errors.xml:6: Unable to create directory as a file already exists with that name: /work/tidepool/build/afile

Mirroring a directory with sync

sync (Ant 1.6) copies like copy, using the same newer-wins rule, but then removes from the target directory every file and directory that none of the nested resource collections produced. It is the right task for a directory that must contain exactly what the build produced, such as a staged site or a distribution folder. The attributes are todir (required), overwrite, includeEmptyDirs, failonerror, verbose and granularity. A nested <preserveInTarget> (since 1.7) protects files that exist only in the target; it is like a fileset without a dir, with preserveEmptyDirs to protect empty directories too. The earlier example mirrored generated into site: stale.html was removed as a dangling file, while keep.me survived because of preserveInTarget. Be careful with todir: sync deletes whatever it does not recognise there.

Setting modification times with touch

touch creates a file if needed and sets its modification time. The time comes from millis (milliseconds since the epoch), from datetime (the special value now, or a date in the form MM/dd/yyyy hh:mm a, as in 01/02/2026 10:00 AM, unless pattern gives another SimpleDateFormat), or, if neither is set, is the current time. Other attributes are mkdirs (create missing parent directories, since 1.6.3) and verbose (log creation, default true). Nested resource collections touch many files at once, and a nested mapper, as in <mapper type="glob" from="foo" to="bar"/>, touches the mapped names, taking the original file’s time when no time is given. touch uses java.io.File, so the resolution depends on the file system; for more control, fall back to exec (Running Programs). Touching a file you do not own only logs a warning on many Unix systems, since Ant 1.8.2.

Reproducible builds use touch or the modificationtime attribute of archive tasks to give files a fixed time; see Packaging Archives.

Concatenating files with concat

concat writes the content of its nested resources to destfile, or to the console when destfile is absent, which makes it a convenient way to print a file. It accepts any resource collection, nested text, a filterchain, and header and footer elements (attributes file, filtering, trim and trimleading; the text may be inline).

Attribute Meaning Default

destfile

the output file; the console when omitted

console

append

append to an existing destfile

false

overwrite (force is deprecated)

write even if destfile is newer than every source (since 1.8.2)

true

forceReadOnly

overwrite a read-only destfile (since 1.8.2)

false

encoding, outputencoding

character sets for input and output

the JVM default character set

fixlastline, eol

add a line ending when a concatenated file lacks one, and which line ending (cr, lf, crlf, mac, unix, dos)

no; eol follows the platform

binary

concatenate byte by byte (requires destfile; cannot be combined with encodings or nested text)

false

filterbeforeconcat

apply the filter chain to each input rather than once to the concatenation (since 1.10.10)

false

ignoreempty

do not create destfile when there is nothing to concatenate (since 1.8.0)

true

Without fixlastline, a file that does not end in a newline runs into the next file. The first part of the build below concatenates a header and two files, the first of which has no final newline; fixlastline keeps them on separate lines. The rest of the build exercises the tasks described next.

text-tasks.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="text-tasks" default="demo" basedir=".">
  <target name="demo">
    <delete dir="build/text"/>
    <mkdir dir="build/text"/>

    <!-- concat: a header, two files (the first lacks a final newline) and fixlastline -->
    <concat destfile="build/text/all.txt" fixlastline="true" eol="lf">
      <header filtering="no" trim="yes">
        == tidepool notes ==
      </header>
      <fileset dir="docs" includes="a.txt,b.txt"/>
    </concat>
    <echo>all.txt:</echo>
    <concat><fileset file="build/text/all.txt"/></concat>

    <!-- fixcrlf: CRLF to LF, in place on a copy -->
    <copy file="docs/crlf.txt" todir="build/text"/>
    <fixcrlf srcdir="build/text" includes="crlf.txt" eol="lf"/>
    <length file="docs/crlf.txt" property="crlf.before"/>
    <length file="build/text/crlf.txt" property="crlf.after"/>
    <echo message="crlf.txt: ${crlf.before} bytes before, ${crlf.after} bytes after"/>

    <!-- replace: plain string, and replacefilter with a property file -->
    <copy file="config/tidepool.properties.in" tofile="build/text/tidepool.properties"/>
    <replace file="build/text/tidepool.properties" summary="true">
      <replacefilter token="@VERSION@" value="2.0.0"/>
      <replacefilter token="@CITY@" value="Sydney"/>
      <replacefilter token="@DATA@" value="data/stations.csv"/>
    </replace>
    <concat><fileset file="build/text/tidepool.properties"/></concat>

    <!-- replaceregexp: rewrite key=value as "key = value", using groups -->
    <replaceregexp file="build/text/tidepool.properties" byline="true"
                   match="^([a-z.]+)=(.*)$" replace="\1 = \2"/>
    <concat><fileset file="build/text/tidepool.properties"/></concat>
  </target>
</project>
demo:
    [mkdir] Created dir: /work/tidepool/build/text
     [echo] all.txt:
   [concat] == tidepool notes ==
   [concat] no newline at end
   [concat] second file
     [copy] Copying 1 file to /work/tidepool/build/text
     [echo] crlf.txt: 14 bytes before, 12 bytes after
     [copy] Copying 1 file to /work/tidepool/build/text
  [replace] Replaced 3 occurrences in 1 files.
   [concat] station.file=data/stations.csv
   [concat] version=2.0.0
   [concat] city=Sydney
   [concat] station.file = data/stations.csv
   [concat] version = 2.0.0
   [concat] city = Sydney
BUILD SUCCESSFUL

The output shows concat (with the header and fixlastline), then fixcrlf, replace and replaceregexp, which are described next. Although a.txt ended without a newline, its text and the text of b.txt are on separate lines.

Normalising line endings with fixcrlf

fixcrlf adjusts text files. It operates in place when only srcdir (or file, since 1.7) is given and writes to destDir otherwise. It is a directory-based task, so includes, excludes, includesfile, excludesfile and defaultexcludes work as on a fileset. Its main attributes:

Attribute Meaning Default

eol

asis, cr, lf, crlf, mac, unix or dos; the preferred way to set line endings (the older cr attribute with add and remove is deprecated)

platform: lf on Unix and macOS, crlf on DOS-style systems

tab, tablength

add, asis or remove for tabs, and the tab width from 2 to 80

asis, 8

javafiles

with tab, leave TAB characters inside Java string and character constants alone

no

eof

add, asis or remove for a DOS end-of-file (control-Z) character

platform: remove on Unix

fixlast

add a missing line ending to the last line (ignored when eof is asis)

true

encoding, outputencoding

character sets for reading and writing

the JVM default character set

preservelastmodified

keep the source’s modification time (since 1.6.3)

false

Since Ant 1.10.14 fixcrlf, replace and replaceregexp try to preserve the file permissions of the files they modify, so running them over a shell script no longer drops its executable bit. The earlier example converted a 14-byte CRLF file into a 12-byte LF file. This is also what the distribution example at the end of this page does for its launcher script, because a shell script copied from a Windows checkout with CRLF line endings fails to start on Unix.

fixcrlf can be used inside a filter chain, as <fixcrlf eol="lf"/>, to normalise text as it passes through copy.

Replacing text with replace and replaceregexp

replace is a directory-based task that replaces a literal string in one file (file) or in the files of a fileset (dir with includes, or any nested resource collection since 1.8.0). token is the text to find and value the replacement, which defaults to the empty string. Ant counts the replacements with summary="true". Text that crosses line boundaries is given with nested <replacetoken> and <replacevalue> elements, which expand properties by default (expandProperties). <replacefilter> elements allow several replacements in one pass and may take their value from a property of the file named by propertyFile (the property attribute). replacefilterfile treats every entry of a properties file as one replacement. preserveLastModified and failOnNoReplacements (both since 1.8.0) control timestamps and whether a run that changed nothing fails.

replaceregexp replaces the text matched by a regular expression with a substitution pattern. match and replace are the expression and the replacement, with \1 to \9 referring to groups; they can be given as nested <regexp> and <substitution> elements to reuse definitions by refid. flags accepts g (global), i (case-insensitive), m (multi-line) and s (single-line). By default the file is processed as a whole; byline="true" processes one line at a time, which is how you replace only the first occurrence on each line. Since Ant 1.10.15 the failonerror attribute makes errors fail the build; before that, and by default, some failures were swallowed and only logged. replaceregexp needs a regular-expression implementation; the default is the JDK’s java.util.regex, which is what the example used.

The earlier text-tasks.xml example replaced three tokens in one replace with a summary (Replaced 3 occurrences in 1 files.) and then rewrote key=value lines as key = value with a regular expression and groups. The next target shows what failOnNoReplacements="true" does when a token is not found:

errors.xml (target replace-nothing)
  <target name="replace-nothing">
    <copy file="data/stations.csv" tofile="build/s.csv"/>
    <replace file="build/s.csv" token="@NOPE@" value="x" failOnNoReplacements="true"/>
  </target>
replace-nothing:
     [copy] Copying 1 file to /work/tidepool/build
BUILD FAILED
/work/tidepool/errors.xml:10: didn't replace anything

Naming, sizing and temporary files

These small tasks set a property or adjust a file; none of them copies content.

Task Behaviour

basename

sets property to the last element of file, optionally without a suffix (with or without the dot)

dirname

sets property to the full directory path of file; unlike the Unix command it returns the absolute directory, and the project’s basedir for a bare name

length

reports the length in bytes of a file, a resource, a string (optionally trim) or nested resource collections; mode is all (the sum, default) or each (one line per resource, written to the log when no property is given); usable as a condition with length and when (equal, eq, greater, gt, less, lt, ge, ne, le)

makeurl

turns file, or nested fileset and path elements, into file: URLs stored in property, joined by separator (a space by default); validate (default true) fails if a named file is missing

tempfile

sets property to the name of a file that does not exist, built from destdir, prefix and suffix; it does not create the file unless createfile="true" (since 1.8), and deleteonexit marks it for deletion when the JVM exits

truncate

sets the file length (suffixes K, M, G, T, P) or changes it by adjust, creating missing files unless create="false" and missing parent directories if mkdirs="true"

props-tasks.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="props-tasks" default="demo" basedir=".">
  <target name="demo">
    <delete dir="build/props"/>
    <mkdir dir="build/props"/>

    <property name="cli.jar" location="build/tidepool-cli-1.0.0.jar"/>
    <basename property="jar.file" file="${cli.jar}"/>
    <basename property="jar.stem" file="${cli.jar}" suffix=".jar"/>
    <dirname property="jar.dir" file="${cli.jar}"/>
    <echo message="file=${jar.file} stem=${jar.stem}"/>
    <echo message="dir=${jar.dir}"/>

    <!-- tempfile only picks a name; createfile="true" also creates the file -->
    <tempfile property="scratch.name" destdir="build/props" prefix="tide-" suffix=".tmp"/>
    <available file="${scratch.name}" property="scratch.exists"/>
    <echo message="scratch exists before use: ${scratch.exists}"/>
    <tempfile property="scratch.created" destdir="build/props" prefix="tide-" suffix=".tmp" createfile="true"/>
    <available file="${scratch.created}" property="scratch.created.exists"/>
    <echo message="created exists: ${scratch.created.exists}"/>

    <!-- length, in its three modes, and as a condition -->
    <length string="tidepool" property="len.string"/>
    <length file="data/stations.csv" property="len.csv"/>
    <length property="len.all" mode="all">
      <fileset dir="data" includes="*.csv"/>
      <fileset dir="docs" includes="a.txt,b.txt"/>
    </length>
    <echo message="string=${len.string} csv=${len.csv} all=${len.all}"/>
    <length mode="each">
      <fileset dir="docs" includes="a.txt,b.txt"/>
    </length>
    <condition property="csv.big">
      <length file="data/stations.csv" length="100" when="gt"/>
    </condition>
    <echo message="stations.csv longer than 100 bytes: ${csv.big}"/>

    <!-- truncate and adjust -->
    <truncate file="build/props/blob.bin" length="1K" create="true"/>
    <truncate file="build/props/blob.bin" adjust="-24"/>
    <length file="build/props/blob.bin" property="len.blob"/>
    <echo message="blob.bin: ${len.blob} bytes"/>

    <!-- makeurl -->
    <makeurl file="data/stations.csv" property="csv.url"/>
    <echo message="url=${csv.url}"/>
    <makeurl property="docs.urls" separator=" | ">
      <fileset dir="docs" includes="*.txt"/>
    </makeurl>
    <echo message="${docs.urls}"/>
    <!-- validate="true" (the default) fails on a missing file -->
    <makeurl file="data/missing.csv" property="missing.url" validate="false"/>
    <echo message="not validated: ${missing.url}"/>
    <makeurl file="data/missing.csv" property="missing2.url"/>
  </target>
</project>
demo:
    [mkdir] Created dir: /work/tidepool/build/props
     [echo] file=tidepool-cli-1.0.0.jar stem=tidepool-cli-1.0.0
     [echo] dir=/work/tidepool/build
     [echo] scratch exists before use: ${scratch.exists}
     [echo] created exists: true
     [echo] string=8 csv=106 all=135
   [length] /work/tidepool/docs/a.txt : 17
   [length] /work/tidepool/docs/b.txt : 12
     [echo] stations.csv longer than 100 bytes: true
     [echo] blob.bin: 1000 bytes
     [echo] url=file:/work/tidepool/data/stations.csv
     [echo] file:/work/tidepool/docs/a.txt | file:/work/tidepool/docs/b.txt | file:/work/tidepool/docs/crlf.txt
     [echo] not validated: file:/work/tidepool/data/missing.csv
BUILD FAILED
/work/tidepool/props-tasks.xml:54: A source file is missing: /work/tidepool/data/missing.csv

Three details are worth reading in that output. The line scratch exists before use: ${scratch.exists} shows what happens when an available test finds nothing: the property is simply not set, so a later reference stays literal instead of being false. tempfile picked a unique name in build/props without creating it, while createfile="true" created it. And the last makeurl failed because validate defaults to true.

Java’s java.io.File can set only the owner’s read, write and execute bits, so Ant offers three ways to manage permissions and two to manage links.

  • chmod runs the Unix chmod executable. It does nothing on other systems unless os names the system, and it takes the usual perm value (755, ugo+rx). It handles many files in one command line, which can become too long: maxparallel limits how many files go into each command. With dir, the permissions change only on the directory itself unless patterns are given. type is file (default), dir or both.

  • setpermissions (Ant 1.10.0) uses Java NIO and works without an external program. mode takes an octal number and permissions a comma-separated list of PosixFilePermission names. With neither, all permissions are removed. nonPosixMode says what to do on a file system without POSIX permissions: fail (default), pass, tryDosOrFail or tryDosOrPass. Since 1.10.16 actOnLinkTargets chooses whether a link’s permissions or its target’s are changed. The files come from nested resource collections, so you select the files with the usual elements.

  • symlink manages symbolic links through java.nio.file.Files since Ant 1.10.2. action is single (default), record, recreate or delete; link and resource name the link and its target; overwrite replaces an existing file. The link attribute is resolved against the current working directory, not the project’s basedir, so write it as ${basedir}/....

  • <delete link="..."/> (1.10.16) deletes a link or junction through the same API on all platforms.

  • mklink (Windows only, Ant 1.10.16) wraps the Windows mklink command to create file-symlink, dir-symlink, hardlink and junction links, from targetFile (resolved against basedir) or targetText (passed through verbatim). Creating symbolic links on Windows needs the "create symbolic link" privilege, for instance through developer mode. mklink was not run here, because the examples ran on macOS.

A small show macro runs ls -l and prints only the mode string and the name. The build lists the two scripts, lets chmod make start.sh executable, lets setpermissions set mode 640 on start.bat and an explicit list of permissions on start.sh, then creates a symbolic link, lists it and deletes it with <delete link>.

perms-links.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="perms-links" default="demo" basedir=".">
  <macrodef name="show">
    <attribute name="dir"/>
    <sequential>
      <exec executable="sh">
        <arg value="-c"/>
        <arg value="ls -l @{dir} | awk 'NR&gt;1 {print substr($1,1,10), $9, $10, $11}'"/>
      </exec>
    </sequential>
  </macrodef>

  <target name="demo">
    <delete dir="build/perm"/>
    <mkdir dir="build/perm/bin"/>
    <touch file="build/perm/bin/start.sh"/>
    <touch file="build/perm/bin/start.bat"/>
    <show dir="build/perm/bin"/>

    <!-- chmod: Unix only, uses the chmod executable -->
    <chmod dir="build/perm/bin" perm="755" includes="**/*.sh"/>
    <show dir="build/perm/bin"/>

    <!-- setpermissions: NIO, portable across POSIX and DOS file systems -->
    <setpermissions mode="640" nonPosixMode="tryDosOrPass">
      <file file="build/perm/bin/start.bat"/>
    </setpermissions>
    <setpermissions permissions="OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_EXECUTE">
      <file file="build/perm/bin/start.sh"/>
    </setpermissions>
    <show dir="build/perm/bin"/>

    <!-- symlink: create, then delete through the link attribute of delete -->
    <symlink link="${basedir}/build/perm/current" resource="bin"/>
    <show dir="build/perm"/>
    <delete link="${basedir}/build/perm/current"/>
    <show dir="build/perm"/>
  </target>
</project>
demo:
    [mkdir] Created dir: /work/tidepool/build/perm/bin
    [touch] Creating /work/tidepool/build/perm/bin/start.sh
    [touch] Creating /work/tidepool/build/perm/bin/start.bat
     [exec] -rw-r--r-- start.bat
     [exec] -rw-r--r-- start.sh
     [exec] -rw-r--r-- start.bat
     [exec] -rwxr-xr-x start.sh
     [exec] -rw-r----- start.bat
     [exec] -rwxr-x--- start.sh
     [exec] drwxr-xr-x bin
     [exec] lrwxr-xr-x current -> bin
   [delete] Deleting: /work/tidepool/build/perm/current
     [exec] drwxr-xr-x bin
BUILD SUCCESSFUL

Since Ant 1.10.16 junctions are treated like symbolic links in general. A Windows build that used followSymlinks="false" and relied on Ant following junctions breaks: set followSymlinks="true" and exclude real symbolic links with the symlink selector (see Selectors).

The deprecated tasks

copyfile, copydir, rename and deltree come from Ant 1.1 and Ant 1.2. They still exist in 1.10.18 but are deprecated and print a message each time they run:

deprecated.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="deprecated" default="demo" basedir=".">
  <target name="demo">
    <delete dir="build/dep"/>
    <mkdir dir="build/dep"/>
    <copyfile src="data/stations.csv" dest="build/dep/a.csv"/>
    <copydir src="docs" dest="build/dep/docs"/>
    <rename src="build/dep/a.csv" dest="build/dep/b.csv"/>
    <deltree dir="build/dep/docs"/>
  </target>
</project>
demo:
    [mkdir] Created dir: /work/tidepool/build/dep
 [copyfile] DEPRECATED - The copyfile task is deprecated.  Use copy instead.
  [copydir] DEPRECATED - The copydir task is deprecated.  Use copy instead.
  [copydir] Copying 3 files to /work/tidepool/build/dep/docs
   [rename] DEPRECATED - The rename task is deprecated.  Use move instead.
  [deltree] DEPRECATED - The deltree task is deprecated.  Use delete instead.
  [deltree] Deleting: /work/tidepool/build/dep/docs
BUILD SUCCESSFUL
Deprecated task Replace it with Note

copyfile src dest

<copy file="..." tofile="..."/>

forceoverwrite becomes overwrite

copydir src dest

<copy todir="..."><fileset dir="..."/></copy>

includes and excludes move onto the fileset

rename src dest

<move file="..." tofile="..."/>

replace (default on) corresponds to overwrite, which is also true by default on move

deltree dir

<delete dir="..."/>

same semantics

Putting it together: staging the tidepool distribution

The tasks combine naturally. This target stages a distribution directory for the tidepool command-line tool: it filters the launcher script’s version into place, fixes its line endings, makes it executable, copies data, generates a configuration file from a template and assembles release notes from fragments. The launcher is deliberately stored with CRLF line endings, as it would be after a checkout on Windows.

stage-dist.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="stage-dist" default="stage" basedir=".">
  <property name="version" value="1.0.0"/>
  <property name="dist" location="build/dist/tidepool-${version}"/>

  <target name="stage">
    <delete dir="build/dist"/>
    <!-- launcher script: filter the version, fix line endings, make it executable -->
    <copy todir="${dist}/bin">
      <fileset dir="src/dist/bin"/>
      <filterset>
        <filter token="VERSION" value="${version}"/>
      </filterset>
    </copy>
    <fixcrlf srcdir="${dist}/bin" eol="lf"/>
    <chmod dir="${dist}/bin" perm="755" includes="*"/>
    <!-- data and configuration -->
    <copy todir="${dist}/data">
      <fileset dir="data"/>
    </copy>
    <copy file="config/tidepool.properties.in" tofile="${dist}/conf/tidepool.properties">
      <filterset>
        <filter token="VERSION" value="${version}"/>
        <filter token="DATA" value="../data/stations.csv"/>
        <filter token="CITY" value="Brest"/>
      </filterset>
    </copy>
    <!-- a release notes file assembled from pieces -->
    <concat destfile="${dist}/RELEASE-NOTES.txt" fixlastline="true">
      <header filtering="no" trim="yes">tidepool ${version}</header>
      <fileset dir="docs" includes="a.txt,b.txt"/>
    </concat>
    <!-- print the result -->
    <pathconvert property="staged" pathsep="${line.separator}  ">
      <fileset dir="${dist}"/>
      <mapper type="regexp" from="^.*/build/dist/(.*)$$" to="\1"/>
    </pathconvert>
    <echo message="staged files:${line.separator}  ${staged}"/>
    <exec executable="sh">
      <arg value="-c"/>
      <arg value="ls -l ${dist}/bin | awk 'NR&gt;1 {print substr($1,1,10), $9}'; tail -n 1 ${dist}/bin/tidepool"/>
    </exec>
  </target>
</project>
stage:
     [copy] Copying 1 file to /work/tidepool/build/dist/tidepool-1.0.0/bin
     [copy] Copying 1 file to /work/tidepool/build/dist/tidepool-1.0.0/data
     [copy] Copying 1 file to /work/tidepool/build/dist/tidepool-1.0.0/conf
     [echo] staged files:
     [echo]   tidepool-1.0.0/RELEASE-NOTES.txt
     [echo]   tidepool-1.0.0/bin/tidepool
     [echo]   tidepool-1.0.0/conf/tidepool.properties
     [echo]   tidepool-1.0.0/data/stations.csv
     [exec] -rwxr-xr-x tidepool
     [exec] exec java -jar "$(dirname "$0")/../lib/tidepool-cli-1.0.0.jar" "$@"
BUILD SUCCESSFUL

The last two lines come from the exec at the end: the launcher has mode 755 (rwxr-xr-x), and its last line shows the @VERSION@ token replaced by 1.0.0. Archiving the staged directory is the job of the tasks on Packaging Archives.

The same jobs in Maven and Gradle

Neither Maven nor Gradle has a task per file operation; both express these jobs differently.

Job Maven 3.10.0 Gradle 9.8.1

copy files, with token filtering

maven-resources-plugin: resources:resources and resources:testResources copy and filter src/main/resources as part of the lifecycle, and resources:copy-resources copies other directories; ${...} and @...@ placeholders are replaced from POM and project properties

Copy tasks and the built-in processResources; filter(ReplaceTokens::class, ...) for @token@ replacement and expand(...) for template expressions

delete generated files

maven-clean-plugin: clean:clean removes target, configurable with filesets

the clean task, a Delete task

mirror a directory

no dedicated goal; maven-antrun-plugin can run Ant’s sync

the Sync task

run Ant file tasks as they are

maven-antrun-plugin

ant.copy(...) and the other Ant tasks through the ant object, see Ant Integration

The Maven side is described on Resources & Filtering and the Gradle side on Working with Files. The migration of an Ant build that stages files this way is on Migrating Between Tools.

Errors you will meet

Message Cause

One of tofile or todir must be set.

a copy with a source but no destination, or a nested fileset and no todir

Cannot concatenate multiple files into a single file.

copy with tofile and a nested fileset that selects more than one file

Warning: Could not find file ... to copy.

the single source file does not exist; with the default failonerror="true" it ends the build as BUILD FAILED

... does not exist.

a fileset’s dir is missing, so the copy over it fails

Unable to create directory as a file already exists with that name

mkdir on a path that is a file

didn't replace anything

replace with failOnNoReplacements="true" found no token

A source file is missing: ...

makeurl with validate left at true

The messages in that table that are not shown above came from these targets:

BUILD FAILED
/work/tidepool/errors.xml:13: One of tofile or todir must be set.
BUILD FAILED
/work/tidepool/errors.xml:16: One of tofile or todir must be set.
BUILD FAILED
/work/tidepool/errors.xml:22: Cannot concatenate multiple files into a single file.
BUILD FAILED
/work/tidepool/errors.xml:19: /work/tidepool/no-such-dir does not exist.

Where older books are out of date

Books written for Ant 1.5, such as the 2002 Java Development with Ant, describe copy, move, delete and filter well, but predate several changes:

  • The nested element of these tasks was a fileset only. Since Ant 1.7 any file-system resource collection can be used, which is why copy can take a path or a filelist.

  • copydir, copyfile, rename and deltree were current when the book was written. They are deprecated now; use copy, move and delete.

  • The book targets JDK 1.3 and 1.4. Ant 1.10 needs Java 8 or later, which gives file tasks such as setpermissions and symlink a portable implementation on java.nio, where older Ant versions called external programs.

  • Character encodings are explicit today. encoding and outputencoding are the attributes to use, and file.encoding of the running JVM is the default; on Java 18 and later that default is UTF-8 (JEP 400), where it used to depend on the platform.

Where to go next