In this post, I’ll walk you through an intriguing Groovy script that I wrote as part of a proof of concept. This script leverages the powerful AntBuilder to transform a custom .antcmd
file into a Groovy script and execute it. This approach can be particularly useful for performing filesystem manipulations where using tools like Terraform or Ansible might be overkill.
Why AntBuilder with Groovy?
Apache Ant is a powerful tool for automating software build processes. By using Groovy’s AntBuilder, we can directly utilize Ant’s tasks within Groovy scripts, making it a flexible solution for various automation needs.
Licensing
This script is licensed under the Apache License, Version 2.0, the same license used by Groovy and Ant. This ensures compatibility and encourages open-source collaboration and use.
The Groovy Script
The following Groovy script accomplishes the following tasks:
- Parse a Custom .antcmd File: The script reads a custom
.antcmd
file and parses the commands. - Generate Groovy Script: It converts the parsed commands into a Groovy script using AntBuilder.
- Execute the Generated Script: Optionally, it can execute the generated Groovy script.
It can also execute a single Antbuilder command on the commandline.
Here’s the complete script with explanations.
/*
* Copyright 2024 Rob Deas
*
* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import groovy.util.AntBuilder
import groovy.cli.commons.CliBuilder
import java.text.SimpleDateFormat
import java.util.Date
// List of supported Ant commands
def supportedCommands = [
'copy', 'delete', 'move', 'mkdir', 'touch', 'zip', 'unzip', 'tar', 'untar',
'jar', 'war', 'java', 'exec', 'ftp', 'scp', 'sshexec', 'xmlvalidate', 'xslt',
'echo', 'chmod', 'chown'
]
// Function to parse the custom file
def parseCommands(file) {
def commands = []
def currentCommand = null
file.eachLine { line, lineNumber ->
line = line.trim()
if (line.isEmpty() || line.startsWith('#')) {
return
}
if (lineNumber == 0) {
if (!line.toUpperCase().startsWith('ANTCMD')) {
throw new IllegalArgumentException("First line must start with 'ANTCMD'.")
}
commands << [name: 'ANTCMD', attributes: line]
} else if (line.endsWith('\\')) {
def linePart = line[0..-2]
if (currentCommand == null) {
currentCommand = linePart
} else {
currentCommand += ' ' + linePart
}
} else {
def completeLine = currentCommand ? currentCommand + ' ' + line : line
def parts = completeLine.split(/\s+/, 2)
if (parts.length > 1) {
commands << [name: parts[0], attributes: parts[1]]
} else {
commands << [name: parts[0], attributes: ""]
}
currentCommand = null
}
}
return commands
}
// Function to convert command to AntBuilder code
def commandToGroovy(command, supportedCommands) {
def builder = new StringBuilder()
if (supportedCommands.contains(command.name)) {
builder.append("ant.${command.name}(${attributesToMap(command.attributes)})\n")
} else {
builder.append("${command.name} ${command.attributes}\n")
}
return builder.toString()
}
// Function to convert attributes string to a map
def attributesToMap(attributes) {
def map = [:]
attributes.split(/\s+/).each { pair ->
def (key, value) = pair.split('=', 2)
map[key] = value
}
return map
}
// Function to display help message
def showHelp(cli, supportedCommands) {
cli.usage()
println """
Supported Ant commands:
${supportedCommands.join(', ')}
Other lines will be treated as normal Groovy code.
Example usage:
groovy antcmd.groovy --input commands.antcmd --output generatedScript.groovy --exec log
groovy antcmd.groovy -x copy src="source.txt" dest="build/destination.txt"
"""
}
// Parse command-line arguments
def cli = new CliBuilder(usage: 'groovy antcmd.groovy [options]')
cli.h(longOpt: 'help', 'Show usage information')
cli.i(longOpt: 'input', args: 1, argName: 'inputFile', 'Input .antcmd file')
cli.o(longOpt: 'output', args: 1, argName: 'outputFile', 'Output Groovy script file')
cli.e(longOpt: 'exec', args: 1, argName: 'execMode', 'Execution mode: off, on')
cli.x(longOpt: 'execute', args: 1, argName: 'command', 'Execute a single Ant command immediately')
def options = cli.parse(args)
if (!options) {
showHelp(cli, supportedCommands)
System.exit(1)
}
if (options.h) {
showHelp(cli, supportedCommands)
return
}
String execMode = options.e ? options.e.toLowerCase() : 'no'
def trueValues = ["yes", "true", "on", "1", "run"]
def generateOnly = !trueValues.contains(execMode.trim())
def inputFile = options.i ? new File(options.i) : new File('commands.antcmd')
def outputFile = options.o ? new File(options.o) : null
if (!inputFile.exists()) {
println "Error: Input file not found."
System.exit(1)
}
def commands
try {
commands = parseCommands(inputFile)
} catch (Exception e) {
println "Error parsing input file: ${e.message}"
System.exit(1)
}
println "Parsed commands: ${commands}" // Debug output
if (!outputFile) {
def dateFormat = new SimpleDateFormat("yyyyMMdd-HHmm-ssSSS")
def timestamp = dateFormat.format(new Date())
outputFile = new File("cmds-${timestamp}.groovy")
}
def outputBuilder = new StringBuilder()
outputBuilder.append("import groovy.util.AntBuilder\n\n")
outputBuilder.append("def ant = new AntBuilder()\n\n")
commands[1..-1].each { command ->
outputBuilder.append(commandToGroovy(command, supportedCommands))
}
try {
outputFile.text = outputBuilder.toString()
println "Groovy script has been generated at ${outputFile.absolutePath}"
println "Output file: ${outputFile.absolutePath}"
} catch (Exception e) {
println "Error writing to output file: ${e.message}"
System.exit(1)
}
if (!generateOnly) {
println "Running the generated Groovy script..."
try {
def scriptText = outputFile.text
new GroovyShell().evaluate(scriptText)
println "Script execution complete."
} catch (Exception e) {
println "Error executing generated script: ${e.message}"
e.printStackTrace()
System.exit(1)
}
}
How It Works
- Parsing the .antcmd File:
- The
parseCommands
function reads and processes each line of the.antcmd
file, handling multiline commands and comments. - It ensures that the file starts with
ANTCMD
and constructs a list of commands.
- The
- Generating the Groovy Script:
- The
commandToGroovy
function converts each command into its corresponding AntBuilder Groovy code. - The
attributesToMap
function helps in converting the command attributes to a map format required by AntBuilder.
- The
- Command-Line Options:
- The script uses
CliBuilder
to parse command-line arguments, allowing users to specify input/output files and execution modes.
- The script uses
- Executing the Script:
- If execution is enabled (
--exec
), the script usesGroovyShell
to evaluate the generated script.
- If execution is enabled (
Sample .antcmd Files
Here are a few sample .antcmd
files to get you started, these particular examples are just for illustration and are not fully tested:
Sample 1: Basic File Operations
ANTCMD
copy src="source.txt" dest="build/destination.txt"
delete file="build/oldfile.txt"
mkdir dir="build/newdir"
Sample 2: Archiving and Extracting Files
ANTCMD
zip destfile="build/archive.zip" basedir="build/newdir"
unzip src="build/archive.zip" dest="extracted"
tar destfile="build/archive.tar" basedir="build/newdir"
untar src="build/archive.tar" dest="extracted_tar"
Sample 3: Create a file
ANTCMD
touch file="build/touchedfile.txt"
println 'This is a normal Groovy code line run after a file was created/updated.'
Sample 4: Advanced Commands
ANTCMD
java classname="com.example.Main" fork="true" args="-arg1 -arg2"
ftp server="ftp.example.com" userid="user" password="password" action="put" remotedir="/remote/dir" localfile="build/archive.zip"
sshexec host="remote.example.com" username="user" password="password" command="ls -l"
Usage Examples
- Generate and run the Groovy script: groovy antcmd.groovy –input commands.antcmd –output generatedScript.groovy –exec run
- Execute a single Ant command directly:
groovy antcmd.groovy -x copy src="source.txt" dest="build/destination.txt"
Conclusion
This script demonstrates the flexibility and power of combining Groovy with AntBuilder for automating tasks. It’s a practical solution for those scenarios where full-blown infrastructure automation tools might be excessive. Feel free to adapt and expand this script to suit your specific needs!