Closed rhauch closed 9 years ago
I'll tackle this one, I think I still have some shell scripts lying around from last season.
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.
@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.
@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.
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
...
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.
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,
}
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 atstrongback/bin
. As long as users added thatstrongback/bin
folder to their path, they'd just have to typestrongback ...
on the command line to use it.Generally this would take the form:
Initially, the two commands might be
process
andhelp
, so you'd also be able to run the data post-processor with:and get minimal help with: