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 |
|---|---|---|
|
copies a file, a directory tree or any file-system resource collection; optional token filtering, filter chains, re-encoding and name mapping |
core task; |
|
moves or renames a file, a directory or a collection of files; same options as |
renaming a directory with |
|
deletes a file, a link, a directory tree or a collection of files |
|
|
creates a directory and any missing parents; does nothing if it exists |
core task |
|
makes a directory an exact mirror of one or more resource collections, deleting what is not in them |
1.6; |
|
creates a file or sets its modification time |
|
|
concatenates resources into a file or to the console |
usable as a resource collection since 1.7.1; |
|
normalises line endings, tabs and the end-of-file character |
usable in a filter chain since 1.7 |
|
replace a string or a regular expression inside text files |
|
|
sets a property to the name of a file that does not exist yet |
|
|
sets or adjusts the length of files |
1.7.1 |
|
set a property from the name, the directory, the size or the URL of a file |
|
|
change file permissions, |
|
|
create and manage symbolic links; |
|
|
older equivalents of |
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.
Source: Ant manual: copy
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 |
|---|---|---|
|
the file to copy; required unless a nested resource collection is used |
none |
|
the destination file or directory; with several sources only |
none (one is required) |
|
copy even when the destination is newer |
|
|
overwrite read-only destination files (since 1.8.2) |
|
|
give the copy the modification time of the source |
|
|
apply the project-wide |
|
|
ignore the directory structure of the sources and put every file straight into |
|
|
copy the empty directories found in nested filesets |
|
|
stop the build when a source is missing or the copy fails; if |
|
|
with |
|
|
log every file copied |
|
|
character set to read and to write when filtering ( |
the JVM default character set |
|
with a |
|
|
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.
<?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
Source: Ant manual: copy
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.
station.file=@DATA@
version=@VERSION@
city=@CITY@
<?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.
<?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
Source: Ant manual: FilterSet
Source: Ant manual: copy, encoding
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.
<?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.
Source: Ant manual: copy
Source: Ant manual: Mapper
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.
<?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.
Source: Ant manual: copy
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.
Source: Ant manual: move
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 |
|---|---|---|
|
what to delete; |
none (one is required unless there is a nested collection) |
|
with nested filesets, also delete directories left empty |
|
|
do not report a missing file or a file that cannot be deleted; implies |
|
|
stop the build when something cannot be deleted |
|
|
log every deleted file |
|
|
if deletion fails, ask the JVM to try again when it exits (since 1.6.2) |
|
|
remove links that were not followed because |
|
|
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.
<?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..
Source: Apache Ant manual: 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.
<?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.
Source: Ant manual: delete
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
Source: Ant manual: mkdir
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.
Source: Ant manual: sync
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.
Source: Ant manual: touch
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 |
|---|---|---|
|
the output file; the console when omitted |
console |
|
append to an existing |
|
|
write even if |
|
|
overwrite a read-only |
|
|
character sets for input and output |
the JVM default character set |
|
add a line ending when a concatenated file lacks one, and which line ending ( |
|
|
concatenate byte by byte (requires |
|
|
apply the filter chain to each input rather than once to the concatenation (since 1.10.10) |
|
|
do not create |
|
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.
<?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.
Source: Ant manual: concat
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 |
|---|---|---|
|
|
platform: |
|
|
|
|
with |
|
|
|
platform: |
|
add a missing line ending to the last line (ignored when |
|
|
character sets for reading and writing |
the JVM default character set |
|
keep the source’s modification time (since 1.6.3) |
|
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.
Source: Ant manual: fixcrlf
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:
<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
Source: Ant manual: replace
Source: Ant manual: replaceregexp
Naming, sizing and temporary files
These small tasks set a property or adjust a file; none of them copies content.
| Task | Behaviour |
|---|---|
|
sets |
|
sets |
|
reports the length in bytes of a |
|
turns |
|
sets |
|
sets the file |
<?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.
Source: Ant manual: length
Source: Ant manual: tempfile
Source: Ant manual: truncate
Source: Ant manual: basename
Source: Ant manual: dirname
Source: Ant manual: makeurl
Permissions and links
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.
-
chmodruns the Unixchmodexecutable. It does nothing on other systems unlessosnames the system, and it takes the usualpermvalue (755,ugo+rx). It handles many files in one command line, which can become too long:maxparallellimits how many files go into each command. Withdir, the permissions change only on the directory itself unless patterns are given.typeisfile(default),dirorboth. -
setpermissions(Ant 1.10.0) uses Java NIO and works without an external program.modetakes an octal number andpermissionsa comma-separated list ofPosixFilePermissionnames. With neither, all permissions are removed.nonPosixModesays what to do on a file system without POSIX permissions:fail(default),pass,tryDosOrFailortryDosOrPass. Since 1.10.16actOnLinkTargetschooses 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. -
symlinkmanages symbolic links throughjava.nio.file.Filessince Ant 1.10.2.actionissingle(default),record,recreateordelete;linkandresourcename the link and its target;overwritereplaces an existing file. Thelinkattribute is resolved against the current working directory, not the project’sbasedir, 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 Windowsmklinkcommand to createfile-symlink,dir-symlink,hardlinkandjunctionlinks, fromtargetFile(resolved againstbasedir) ortargetText(passed through verbatim). Creating symbolic links on Windows needs the "create symbolic link" privilege, for instance through developer mode.mklinkwas 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>.
<?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>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).
Source: Ant manual: chmod
Source: Ant manual: setpermissions
Source: Ant manual: symlink
Source: Ant manual: mklink
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:
<?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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
same semantics |
Source: Ant manual: copyfile
Source: Ant manual: copydir
Source: Ant manual: rename
Source: Ant manual: deltree
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.
<?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>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.
Source: Apache Ant manual: Copy, FixCRLF, Chmod and Concat
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 |
|
|
delete generated files |
|
the |
mirror a directory |
no dedicated goal; |
the |
run Ant file tasks as they are |
|
|
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.
Source: Maven Clean Plugin: clean:clean
Source: Maven AntRun Plugin
Errors you will meet
| Message | Cause |
|---|---|
|
a |
|
|
|
the single source file does not exist; with the default |
|
a fileset’s |
|
|
|
|
|
|
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
copycan take apathor afilelist. -
copydir,copyfile,renameanddeltreewere current when the book was written. They are deprecated now; usecopy,moveanddelete. -
The book targets JDK 1.3 and 1.4. Ant 1.10 needs Java 8 or later, which gives file tasks such as
setpermissionsandsymlinka portable implementation onjava.nio, where older Ant versions called external programs. -
Character encodings are explicit today.
encodingandoutputencodingare the attributes to use, andfile.encodingof 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.
Source: Ant manual: copy
Source: JEP 400: UTF-8 by Default
Where to go next
-
Paths, Filesets & Patterns for the patterns and default excludes that every task above relies on.
-
Filters & Mappers for filter sets, filter chains and the mapper types.
-
Packaging Archives to turn a staged directory into JARs, ZIPs and TARs.
-
Running Programs for
execandapply, which run an external program over files. -
Utility Tasks for
echo,loadfile,availableand the other helpers used in the examples. -
Properties for how the properties set by these tasks behave.