strongback / strongback-java

A library for FIRST Robotics Competition robots that makes it easier to write and test your robot code.
MIT License
41 stars 38 forks source link

Add 'strongback' executable #6

Closed rhauch closed 9 years ago

rhauch commented 9 years ago

At this time there is only one thing that might correspond to a Strongback "executable": running the data post-processor (see issue #7). But we could easily plan ahead and make room for more.

We should create a "strongback" executable script that can do a number of different things. We'd need a bourne shell script for Linux and OS X and a separate but equivalent Windows batch file. These could be kept in a script top-level project/folder in our repository and installed into the ZIP file at strongback/bin. As long as users added that strongback/bin folder to their path, they'd just have to type strongback ... on the command line to use it.

Generally this would take the form:

strongback <command> <command-param-1> <command-param-2> ...

Initially, the two commands might be process and help, so you'd also be able to run the data post-processor with:

strongback process <path-to-data-file> <optional-path-to-output-file>

and get minimal help with:

strongback help
Zabot commented 9 years ago

I'll tackle this one, I think I still have some shell scripts lying around from last season.

rhauch commented 9 years ago

Another idea @Zabot had yesterday was to automatically add strongback to an Eclipse project created by the WPILib plugin.

strongback add-project <path-to-eclipse-project-dir>

See #10 for details.

rhauch commented 9 years ago

@Zabot, have you been able to make progress on this? This is blocking us from releasing, so it'd be great if we could get something soon.

Zabot commented 9 years ago

@rhauch Cleanly parsing the command line is difficult, especially trying to do it with shell scripts. Have a look at Zabot/strongback-java@e8961b1c08ea4c83ee8b85077cfef17bfccc8e15. I refactored LogDecoder to use the Apache Commons CLI Library and mushed some things about. I'm still trying to decide if any of that made it easier to read, but I'm thinking the best way to do the rest of the executables is using Java and Commons CLI in the same style. A separate file for each executable (new-project, add-project, etc.), each with its own main(), and then a single Strongback.java with its own main() that can delegate to them. That gives us the flexibility to distribute everything as either one jar, or as separate independent jars. Doing things in Java now may also make it easier to migrate to an eclipse plugin, or not, I've never looked at it.

Zabot commented 9 years ago

Distribution directory structure with single jar

Strongback
    bin
        strongback.sh
        strongback.bat
    libs
        strongback-exec.jar
    java
        libs
            strongback.jar
            strongback-testing.jar
        ...

Distribution directory structure with independent jars

Strongback
    bin
        new-project.sh
        new-project.bat
        convert-project.sh
        convert-project.bat
        log-decoder.sh
        log-decoder.bat
    libs
        new-project.jar
        convert-project.jar
        log-decoder.jar
    java
        libs
            strongback.jar
            strongback-testing.jar
        ...
Zabot commented 9 years ago

Alternatively we can drop command line parameters and do it with a shell/bat script that expects an exact syntax. Its a little more difficult for the user, but substantially easier to maintain.

rhauch commented 9 years ago

We should use command line parameters, but I'd rather avoid bringing a 3rd party library just to do this. For bash scripts, we should use getopts, which works really well and is pretty straightforward. (Note we should not use getopt because it doesn't handle args with whitespace.)

For Windows, we can pretty easily parse them, though we may have to add extra logic if we want the usage to be similar to bash.

For Java, I have a utility class that does this, and it has a number of convenience methods to convert the string values to a few primitive types. I've not used it on strongback yet, but feel free to use it:

/**
 * Utility for parsing and accessing the command line options and parameters.
 * 
 * @author Randall Hauch
 */
@Immutable
public class CommandLineOptions {

    /**
     * Parse the array of arguments passed to a Java {@code main} method and create a {@link CommandLineOptions} instance.
     * 
     * @param args the {@code main} method's parameters; may not be null
     * @return the representation of the command line options and parameters; never null
     */
    public static CommandLineOptions parse(String[] args) {
        Map<String, String> options = new HashMap<>();
        List<String> params = new ArrayList<>();
        List<String> optionNames = new ArrayList<>();
        String optionName = null;
        for (String value : args) {
            value = value.trim();
            if (value.startsWith("-")) {
                optionName = value;
                options.put(optionName, "true");
                optionNames.add(optionName);
            } else {
                if (optionName != null)
                    options.put(optionName, value);
                else
                    params.add(value);
            }
        }
        return new CommandLineOptions(options, params, optionNames);
    }

    private final Map<String, String> options;
    private final List<String> params;
    private final List<String> orderedOptionNames;

    private CommandLineOptions(Map<String, String> options, List<String> params, List<String> orderedOptionNames) {
        this.options = options;
        this.params = params;
        this.orderedOptionNames = orderedOptionNames;
    }

    /**
     * Determine if the option with the given name was used on the command line.
     * <p>
     * The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
     * such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @return true if the exact option was used, or false otherwise or if the supplied option name is null
     * @see #hasOption(String, String)
     */
    public boolean hasOption(String name) {
        return hasOption(name, null);
    }

    /**
     * Determine if the option with one of the given names was used on the command line.
     * <p>
     * The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
     * any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param alternativeName an alternative name for the option; may be null
     * @return true if the exact option was used, or false otherwise or if both the supplied option name and alternative name are
     *         null
     * @see #hasOption(String)
     */
    public boolean hasOption(String name, String alternativeName) {
        return getOption(name, alternativeName, null) != null;
    }

    /**
     * Obtain the value associated with the named option.
     * <p>
     * The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
     * such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @return the value associated with the option, or null if no option was found or the name is null
     * @see #getOption(String, String)
     * @see #getOption(String, String, String)
     */
    public String getOption(String name) {
        return getOption(name, null);
    }

    /**
     * Obtain the value associated with the named option, using the supplied default value if none is found.
     * <p>
     * The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
     * such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or the name is null
     * @see #getOption(String)
     * @see #getOption(String, String, String)
     */
    public String getOption(String name, String defaultValue) {
        return getOption(name, null, defaultValue);
    }

    /**
     * Obtain the value associated with the option given the name and alternative name of the option, using the supplied default
     * value if none is found.
     * <p>
     * The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
     * any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param alternativeName an alternative name for the option; may be null
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or if both the name and alternative
     *         name are null
     * @see #getOption(String)
     * @see #getOption(String, String)
     */
    public String getOption(String name, String alternativeName, String defaultValue) {
        recordOptionUsed(name, alternativeName);
        String result = options.get(name.trim());
        if (result == null && alternativeName != null) result = options.get(alternativeName.trim());
        return result != null ? result : defaultValue;
    }

    protected void recordOptionUsed(String name, String alternativeName) {
        orderedOptionNames.remove(name.trim());
        if (alternativeName != null) orderedOptionNames.remove(alternativeName.trim());
    }

    /**
     * Obtain the long value associated with the named option, using the supplied default value if none is found or if the value
     * cannot be parsed as a long value.
     * <p>
     * The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
     * such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or the name is null
     * @see #getOption(String, String, long)
     */
    public long getOption(String name, long defaultValue) {
        return getOption(name, null, defaultValue);
    }

    /**
     * Obtain the long value associated with the option given the name and alternative name of the option, using the supplied
     * default value if none is found or if the value cannot be parsed as a long value.
     * <p>
     * The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
     * any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param alternativeName an alternative name for the option; may be null
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or if both the name and alternative
     *         name are null
     * @see #getOption(String, long)
     */
    public long getOption(String name, String alternativeName, long defaultValue) {
        return Strings.asLong(getOption(name, alternativeName, null), defaultValue);
    }

    /**
     * Obtain the integer value associated with the named option, using the supplied default value if none is found or if the
     * value
     * cannot be parsed as an integer value.
     * <p>
     * The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
     * such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or the name is null
     * @see #getOption(String, String, int)
     */
    public int getOption(String name, int defaultValue) {
        return getOption(name, null, defaultValue);
    }

    /**
     * Obtain the integer value associated with the option given the name and alternative name of the option, using the supplied
     * default value if none is found or if the value cannot be parsed as an integer value.
     * <p>
     * The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
     * any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param alternativeName an alternative name for the option; may be null
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or if both the name and alternative
     *         name are null
     * @see #getOption(String, int)
     */
    public int getOption(String name, String alternativeName, int defaultValue) {
        return Strings.asInt(getOption(name, alternativeName, null), defaultValue);
    }

    /**
     * Obtain the boolean value associated with the named option, using the supplied default value if none is found or if the
     * value
     * cannot be parsed as a boolean value.
     * <p>
     * The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
     * such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or the name is null
     * @see #getOption(String, String, boolean)
     */
    public boolean getOption(String name, boolean defaultValue) {
        return getOption(name, null, defaultValue);
    }

    /**
     * Obtain the boolean value associated with the option given the name and alternative name of the option, using the supplied
     * default value if none is found or if the value cannot be parsed as a boolean value.
     * <p>
     * The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
     * any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
     * 
     * @param name the name for the option (e.g., "-v" or "--verbose")
     * @param alternativeName an alternative name for the option; may be null
     * @param defaultValue the value that should be returned no named option was found
     * @return the value associated with the option, or the default value if none was found or if both the name and alternative
     *         name are null
     * @see #getOption(String, boolean)
     */
    public boolean getOption(String name, String alternativeName, boolean defaultValue) {
        return Strings.asBoolean(getOption(name, alternativeName, null), defaultValue);
    }

    /**
     * Obtain the parameter at the given index. Parameters are those arguments that are not preceded by an option name.
     * 
     * @param index the index of the parameter
     * @return the parameter value, or null if there was no parameter at the given index
     */
    public String getParameter(int index) {
        return index < 0 || index >= params.size() ? null : params.get(index);
    }

    /**
     * Determine whether the specified value matches one of the parameters.
     * 
     * @param value the parameter value to match
     * @return true if one of the parameter matches the value, or false otherwise
     */
    public boolean hasParameter(String value) {
        return value == null || params.isEmpty() ? false : params.contains(value);
    }

    /**
     * Determine whether there were any unknown option names after all possible options have been checked via one of the
     * {@code getOption(String,...)} methods.
     * 
     * @return true if there was at least one option (e.g., beginning with '{@code -}') that was not checked, or false
     * if there were no unknown options on the command line
     * @see #getFirstUnknownOptionName()
     * @see #hasUnknowns()
     */
    public boolean hasUnknowns() {
        return !orderedOptionNames.isEmpty();
    }

    /**
     * Get the list of unknown option names after all possible options have been checked via one of the
     * {@code getOption(String,...)} methods.
     * 
     * @return the list of options (e.g., beginning with '{@code -}') that were not checked; never null but possible empty
     * @see #getFirstUnknownOptionName()
     * @see #hasUnknowns()
     */
    public List<String> getUnknownOptionNames() {
        return Collections.unmodifiableList(orderedOptionNames);
    }

    /**
     * If there were {@link #hasUnknowns() unknown options}, return the first one that appeared on the command line.
     * @return the first unknown option, or null if there were no {@link #hasUnknowns() unknown options}
     * @see #getUnknownOptionNames()
     * @see #getFirstUnknownOptionName()
     */
    public String getFirstUnknownOptionName() {
        return orderedOptionNames.isEmpty() ? null : orderedOptionNames.get(0);
    }

    @Override
    public String toString() {
        StringJoiner joiner = new StringJoiner(" ");
        options.forEach((opt, val) -> {
            joiner.add("-" + opt).add(val);
        });
        params.forEach(joiner::add);
        return joiner.toString();
    }

}

and an example that uses it:

                final CommandLineOptions options = CommandLineOptions.parse(args);
                if (options.getOption("-?", "--help", false) || options.hasParameter("help")) {
                    printUsage();
                    return status.complete(ReturnCode.SUCCESS);
                }
                if (options.hasOption("--version")) {
                    print(getClass().getSimpleName() + " version " + version);
                    return status.complete(ReturnCode.SUCCESS);
                }
                final String pathToConfigFile = options.getOption("-c", "--config", "config.json");
                verbose = options.getOption("-v", "--verbose", false);

                config = Configuration.load(pathToConfigFile, classLoader, this::printVerbose);
                if (config.isEmpty()) {
                    print("Unable to read configuration file at '" + pathToConfigFile + "': file not found");
                    return status.complete(ReturnCode.UNABLE_TO_READ_CONFIGURATION);
                }

                printVerbose("Found configuration at " + pathToConfigFile + ":");
                printVerbose(config);

                // Adjust the properties by setting any system properties to the configuration ...
                printVerbose("Applying system properties to configuration");
                config = config.withSystemProperties(DEFAULT_SYSTEM_PROPERTY_NAME_PREFIX);
                printVerbose(config);

The constant and enum are defined as:

    private static final String DEFAULT_SYSTEM_PROPERTY_NAME_PREFIX = "STRONGBACK_";

    protected static enum ReturnCode {
        SUCCESS, UNABLE_TO_READ_CONFIGURATION, CONFIGURATION_ERROR, ERROR_DURING_EXECUTION,
    }