Exploring AntBuilder with Groovy

Posted by:

|

On:

|

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:

  1. Parse a Custom .antcmd File: The script reads a custom .antcmd file and parses the commands.
  2. Generate Groovy Script: It converts the parsed commands into a Groovy script using AntBuilder.
  3. 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

  1. 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.
  2. 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.
  3. Command-Line Options:
    • The script uses CliBuilder to parse command-line arguments, allowing users to specify input/output files and execution modes.
  4. Executing the Script:
    • If execution is enabled (--exec), the script uses GroovyShell to evaluate the generated script.

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!

Posted by

in