3
\$\begingroup\$

I've written this shell script to place Ant build.xml and build.properties into my project directory. It takes two options, one which ensures the project is set up for JUnit test compilation/running. The other modifies java compile/execution targets for JavaFX. A name for the project node in build.xml can also be given.

It's a glorified copy/paste from starter Ant files into the project directory because I only have options to support two libraries. I want it for future use when I'm creating a wider variety of projects.

It's my first shell script apart from its previous version which used entirely sed commands rather than xmlstarlet.

createantbuild.sh

#!/bin/bash
# Creates Ant build.xml and build.properties files for a new project in the
# current directory.

helpText="Creates Ant build.xml and build.properties files for a new project in
the current directory.
Usage: createantbuild.sh [<options>] [<project-name>]

where <options> are
   -f, --javafx - include JavaFX modules in compilation and execution tasks
   -u, --junit  - provide JUnit-related Ant targets: compile-tests and test
       --help   - print help

If no options are given, the basic Ant targets are provided. They are:
usage (default), init, compile, execute, clean, and generate-javadoc.
If no project name is given, the Ant project will be the same name as the
current directory."

# Process arguments
for ARG in "$@"; do
    case $ARG in
    "--help")
        echo "$helpText"
        exit 0
        ;;
    "-f" | "--javafx")
        provideJavaFX="set"
        echo "Debug: Processed JavaFX option \"$ARG\"."
        ;;
    "-u" | "--junit")
        provideJUnit="set"
        echo "Debug: Processed JUnit option \"$ARG\"."
        ;;
    "-" | "--")
        echo "Error: Expected option, but none given after \"$ARG\"."
        exit 1
        ;;
    *)
        if [ "${ARG:0:2}" = "--" ]; then
            # A long option that's not recognized
            echo "Error: Unexpected option \"$ARG\"."
            exit 1
        elif [ "${ARG:0:1}" = "-" ]; then
            # Iterate single character options
            for ((a = 1; a < ${#ARG}; a++)); do
                singleCharOption=${ARG:a:1}
                case $singleCharOption in
                "f")
                    provideJavaFX="set"
                    echo "Debug: Processed JavaFX option \"-$singleCharOption\"."
                    ;;
                "u")
                    provideJUnit="set"
                    echo "Debug: Processed JUnit option \"-$singleCharOption\"."
                    ;;
                *)
                    echo "Error: Unexpected option \"-$singleCharOption\"."
                    exit 1
                    ;;
                esac
            done
        elif [ -z "$projectName" ]; then
            # This argument must be the project name
            projectName=$ARG
            echo "Debug: Set project name to \"$ARG\"."
        else
            # Any other non-option arguments are in error
            echo "Error: Unexpected option \"$ARG\"."
            exit 1
        fi
        ;;
    esac
done

# Delete existing Ant files
[ -e build.xml ] && rm build.xml
[ -e build.properties ] && rm build.properties

# Copy in starter Ant files
cp ~/bin/build.xml ./build.xml
cp ~/bin/build.properties ./build.properties

# Set project name to same as directory if necessary
if [ ${#projectName} = 0 ] ; then
    directoryName=`pwd`
    directoryName=${directoryName##*/}
    projectName=$directoryName
    echo "Debug: Set project name to the current directory, \"${projectName}\"."
fi

# Update value of name attribute in project node
xmlstarlet ed -L -u '/project[@name="unset"]/@name' \
    -v "$projectName" build.xml

# Delete JavaFX-related content
if [ "$provideJavaFX" != "set" ]; then
    # Delete elements in build.xml
    xmlstarlet ed -L -d \
        '/project/target[@name="compile"]/javac/compilerarg' build.xml
    xmlstarlet ed -L -d '/project/target[@name="execute"]/java/jvmarg' \
        build.xml
    # Delete lines in build.properties
    sed -i 18,21d ./build.properties
fi

# Delete JUnit-related content
if [ "$provideJUnit" != "set" ]; then
    # Delete elements in build.xml
    xmlstarlet ed -L -d '/project/path[@id="classpath.test"]' build.xml
    xmlstarlet ed -L -d \
        '/project/target[@name="usage"]/echo[@id="compile-tests"]' \
        build.xml
    xmlstarlet ed -L -d '/project/target[@name="usage"]/echo[@id="test"]' \
        build.xml
    xmlstarlet ed -L -d '/project/target[@name="usage"]/echo[@id="blank"]' \
        build.xml
    # shellcheck disable=SC2016
    xmlstarlet ed -L -d \
        '/project/target[@name="init"]/mkdir[@dir="${test.src.dir}"]' \
        build.xml
    xmlstarlet ed -L -d '/project/target[@name="compile-tests"]' build.xml
    xmlstarlet ed -L -d '/project/target[@name="test"]' build.xml
    # shellcheck disable=SC2016
    xmlstarlet ed -L -d \
        '/project/target[@name="clean"]/delete[@dir="${test.build.dir}"]' \
        build.xml
    # Delete lines in build.properties
    sed -i 10,17d ./build.properties
fi

exit 0

These are the the starting Ant files the script acts on after it copies them into the project directory

build.xml

<?xml version = "1.0"?>

<project name="unset" xmlns:if="ant:if" xmlns:unless="ant:unless"
         default="usage">
    <property environment="env"/>
    <property file="build.properties"/>

    <path id="classpath.execute">
        <pathelement location="${main.build.dir}"/>
    </path>

    <path id="classpath.test">
        <pathelement location="${lib.dir}/junit4.jar"/>
        <pathelement location="${main.build.dir}"/>
    </path>

    <target name="usage">
        <!-- List available targets -->
        <echo message="${ant.project.name} targets:"/>
        <echo message=""/>

        <echo message="usage            –> Display this target listing"/>
        <echo message="init             –> Create project directory structure"/>
        <echo message=""/>

        <echo message="compile          –> Compile application sources"/>
        <echo message='execute -Dmain="package.subpackage.ClassName"'/>
        <echo message="                 –> Execute program at a provided main"/>
        <echo message=""/>

        <echo id="compile-tests"
              message="compile-tests    –> Compile unit test sources"/>
        <echo id="test"
              message="test             –> Execute unit tests"/>
        <echo id="blank" message=""/>

        <echo message="clean            –> Clean output directories"/>
        <echo message="generate-javadoc –> Generate javadocs"/>
    </target>

    <target name="init">
        <!-- Create project directory structure -->
        <mkdir dir="${main.src.dir}"/>
        <mkdir dir="${test.src.dir}"/>
    </target>

    <target name="compile">
        <!-- Compile application sources -->
        <mkdir dir="${main.build.dir}"/>
        <javac srcdir="${main.src.dir}" destdir="${main.build.dir}"
               includeantruntime="false" debug="true">
            <compilerarg line="--module-path ${path.to.fx}"/>
            <compilerarg line="--add-modules javafx.controls"/>
        </javac>
    </target>

    <target name="execute">
        <!-- Execute program at a provided main -->
        <java if:set="main" fork="true" classname="${main}">
            <jvmarg line="--module-path ${path.to.fx}"/>
            <jvmarg line="--add-modules javafx.controls"/>
        </java>
        <echo unless:set="main" message='Error: Execute requires the program entry point as a parameter.'/>
        <echo unless:set="main" message='E.g. ant execute -Dmain="package.subpackage.ClassName"'/>
    </target>

    <target name="compile-tests" depends="compile">
        <!-- Compile unit test sources -->
        <mkdir dir="${test.build.dir}"/>
        <javac srcdir="${test.src.dir}" destdir="${test.build.dir}"
               includeantruntime="false" debug="true">
            <classpath refid="classpath.test"/>
        </javac>
    </target>

    <target name="test" depends="compile-tests">
        <!-- Execute unit tests -->
        <junit printsummary="yes" haltonfailure="yes" haltonerror="yes">
            <classpath>
                <path refid="classpath.test"/>
                <pathelement location="${test.build.dir}"/>
            </classpath>
            <formatter type="plain" usefile="false"/>
            <batchtest>
                <fileset dir="${test.src.dir}" includes="**/*Test.java"/>
            </batchtest>
        </junit>
    </target>

    <target name="clean">
        <!-- Clean output directories -->
        <delete dir="${main.build.dir}"/>
        <delete dir="${test.build.dir}"/>
    </target>

    <!--Generate documentation-->
    <target name="generate-javadoc">
        <javadoc packagenames="*" sourcepath="${main.src.dir}"
                 destdir="doc" version="true">
        </javadoc>
    </target>
</project>

build.properties

### Common ###
# Directory for main program sources
main.src.dir = src
# Root output directory for built class files
build.dir = build
# Output directory for built application classes
main.build.dir = ${build.dir}/classes
# Directory for generated documentation
doc.dir = doc

### JUnit ###
# Directory for test java sources
test.src.dir = test
# Output directory for built test classes
test.build.dir = ${build.dir}/test-classes
# Directory where JUnit library is located
lib.dir = /usr/share/java

### JavaFX ###
# Path to JavaFX library
path.to.fx = ${env.PATH_TO_FX}

Output

The bulk of the shell script works with arguments so I included test runs to confirm they are processed and bad arguments are acknowledged.

~/test$ cab
Debug: Set project name to the current directory, "test".
~/test$ cab ""
Debug: Set project name to "".
Debug: Set project name to the current directory, "test".
~/test$ cab myproject
Debug: Set project name to "myproject".
~/test$ cab myproject --
Debug: Set project name to "myproject".
Error: Expected option, but none given after "--".
~/test$ cab --javafx --junit myproject
Debug: Processed JavaFX option "--javafx".
Debug: Processed JUnit option "--junit".
Debug: Set project name to "myproject".
~/test$ cab -f myproject
Debug: Processed JavaFX option "-f".
Debug: Set project name to "myproject".
~/test$ cab -f -u
Debug: Processed JavaFX option "-f".
Debug: Processed JUnit option "-u".
Debug: Set project name to the current directory, "test".
~/test$ cab -fu
Debug: Processed JavaFX option "-f".
Debug: Processed JUnit option "-u".
Debug: Set project name to the current directory, "test".
~/test$ cab myproject whoops
Debug: Set project name to "myproject".
Error: Unexpected option "whoops".
~/test$ cab -b
Error: Unexpected option "-b".
~/test$ cab --what
Error: Unexpected option "--what".
\$\endgroup\$
  • 1
    \$\begingroup\$ I'm honestly not quite sure why you're creating ant builds for new projects... As it stands, maven and gradle are more widespread (and IMO easier to use) alternatives that should support all your usecases \$\endgroup\$ – Vogel612 Sep 1 '19 at 22:30
  • \$\begingroup\$ I'm only using/learning Java at the moment. I figured it would be easier to find info relating to my immediate issues if I focused on a Java-centric tool. It has been successful as I have been able to rapidly find solutions to issues with the documentation alone. I will use a more general tool surely as I move to C++ and when I begin working with a variety of open source projects. \$\endgroup\$ – Nate B Sep 2 '19 at 7:03
3
\$\begingroup\$

Commandline Syntax

You seem to be using the POSIX and GNU command line syntax, allowing both short as long options:

"-f" | "--javafx"

But the most important one "help" is only accessible through:

 "--help"

I would be consistent and use:

 "-h" | "--help" | "-?"

Also note that the question mark is a universal identifier for the help command.

You may also want to add a disclaimer or about info:

 "--about" | "-!" 

And maybe some version info:

 "--version" | "-v" 

Keep in mind that your empty option check treats the operand delimiter -- as an invalid option. While in many systems, this means all remaining arguments are to be treated as operands, even if prefixed with - or --.

"-" | "--"
\$\endgroup\$
  • 1
    \$\begingroup\$ I appreciate that manual resource. I wasn't familiar with it nor some of these common option short forms. I was mimicking the options of rm on my system which only lists the long forms of --help and --version, but I now know secretly implements -h. The empty -- is good to know. I can see why it exists, and I'll keep it in mind. \$\endgroup\$ – Nate B Sep 3 '19 at 0:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.