blob: dbb1a00521254cd63c922229898cd3e4c66b323c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.uima.ducc.cli;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.uima.ducc.common.utils.DuccPropertiesResolver;
public class CommandLine
{
private String[] args; // incoming args from java command line
private IUiOption[] opts; // options defined for this command
private Map<String, IUiOption> name_to_option; // name to IUiOpton map
private Map<IUiOption, String> option_to_value; // UiOption to value map
private Map<IUiOption, IUiOption> legal_options;
private Properties properties;
private int help_width = 100; // justify help strings to about this
/**
* Construct a CommandLine from incoming strings and the legal options.
*
* @param args
* @param opts
*/
public CommandLine(String[] args, IUiOption[] opts)
{
this(args, opts, null);
}
/**
* Construct a CommandLine from the incoming strings, the legal options, and a
* properties file. If the properties file is supplied, it is used to fill in
* options if not otherwise on the command line. The command-line strings always
* take precedence.
*
* @param args
* @param opts
* @param props
*/
public CommandLine(String[] args, IUiOption[] opts, Properties props)
{
this.args = args;
this.opts = opts;
this.properties = props;
this.name_to_option = new HashMap<String, IUiOption>();
this.option_to_value = new HashMap<IUiOption, String>();
this.legal_options = new HashMap<IUiOption, IUiOption>();
for ( IUiOption o : opts ) {
name_to_option.put(o.pname(), o); // String --> option mapping
legal_options.put(o, o); // quick lookup for legal options
if ( o.sname() != null ) { // if it has a short name, point into the same place
name_to_option.put(o.sname(), o);
}
}
}
/**
* Returns whether the parsed command line contained the specified option (by IUiOption).
*
* @param opt This is the option to test for.
*
* @return <b>true</b> if the specified option was found in the parsed command line, <b>false</b> otherwise
*/
public boolean contains(IUiOption opt)
{
// does the command contain this option?
return option_to_value.containsKey(opt);
}
/**
* Returns whether the parsed command line contained the specified option (by string name).
*
* @param opt This is the option to test for.
*
* @return <b>true</b> if the specified option was found in the parsed command line, <b>false</b> otherwise
*/
public boolean contains(String opt)
{
// does the command contain this option?
IUiOption o = name_to_option.get(opt); // see if it's known
if ( o == null ) return false; // not known
return contains(o); // does it have a value?
}
/**
* Checks if this is a command-line option, i.e. with -- or - prefix
*
* @param s This is the command-line token to test
* @return <b>true</b> if it is a valid option, <b>false</b> otherwise
*/
public boolean isOption(String s)
{
// Command-line options must have a -- or - prefix
if ( s.startsWith("--") ) s = s.substring(2);
else if ( s.startsWith("-") ) s = s.substring(1);
else return false;
return name_to_option.containsKey(s);
}
/**
* Checks if this is a valid option
*
* @param s This is the option to test
* @return <b>true</b> if it is a valid option, <b>false</b> otherwise
*/
public boolean isOptionName(String s)
{
return name_to_option.containsKey(s);
}
public boolean isOption(IUiOption k)
{
// is this a legal option?
return legal_options.containsKey(k);
}
/**
* Returns the string value parsed from the command for the specified option, or <b>null</b> if the
* option was not in the command line.
*
* @param opt This is the option to look for.
*
* @return the parsed value from the command line for the option or <b>null</b> if the option was not in the
* command line.
*
* @throws IllegalArgumentException if the option was not found in the command line.
*/
public String get(IUiOption k)
throws IllegalArgumentException
{
// what was the parsed value of this opt
if ( legal_options.containsKey(k) ) {
return option_to_value.get(k);
}
throw new IllegalArgumentException("Option '" + k.pname() + "' is not a legal option.");
}
/**
* Returns the map of all options found in the command, keyed on the corresponding UiOption.
* @return Map of options found in the command line.
*/
public Map<IUiOption, String> allOptions()
{
return option_to_value;
}
public int getInt(IUiOption k)
throws IllegalArgumentException,
NumberFormatException
{
// Note - get() checks for existance. parse() would have filled in defaults.
return Integer.parseInt(get(k));
}
public long getLong(IUiOption k)
throws IllegalArgumentException,
NumberFormatException
{
// Note - get() checks for existance. parse() would have filled in defaults.
return Long.parseLong(get(k));
}
public boolean getBoolean(IUiOption k)
throws IllegalArgumentException
{
// Note - get() checks for existance. parse() would have filled in defaults.
// String values for boolean can be confusing because, Language. So it's useful
// and friendly to be quite permissive:
// Any capitilaztion of true, t, yes, y, 1 --------> true
// Any capitilaztion of false, f, no, n, 0 --------> false
String v = get(k);
if ( v == null || v.isEmpty()) return true;
v = v.toUpperCase();
if ( v.equals("TRUE") || v.equals("T") || v.equals("YES") || v.equals("Y") || v.equals("1") ) return true;
if ( v.equals("FALSE") || v.equals("F") || v.equals("NO") || v.equals("N") || v.equals("0") ) return false;
throw new IllegalArgumentException("Value is not true | false for argument " + k.pname());
}
private void add(IUiOption k, String v)
{
if ( contains(k) ) {
throw new IllegalArgumentException("Duplicate argument " + k.pname() + " not allowed.");
}
option_to_value.put(k,v);
}
private String justify(int leader, String txt)
{
int real_width = help_width - leader;
String blanks = String.format(("%" + leader + "s"), " ");
if ( txt.length() < real_width ) { // trivial case, the string fits with no splits
return blanks + txt;
}
List<String> pieces_parts = new ArrayList<String>();
while(txt.length() > real_width) {
int lastBlank = txt.lastIndexOf(' ', real_width);
pieces_parts.add(txt.substring(0, lastBlank));
txt = txt.substring(lastBlank+1);
}
pieces_parts.add(txt);
StringBuffer sb = new StringBuffer();
for ( String s : pieces_parts ) {
sb.append(blanks);
sb.append(s);
sb.append("\n");
}
return sb.toString();
}
/**
* Formats the options into a help screen.
* @return The formatted help string.
*/
public String formatHelp(String commandName)
{
//
// Strategy
// - try to keep line length to witnin maybe 100 chars by wrapping
// - get width of widest opt to set first column
// - then set argname, required, and default
// - on a new line do justified description with indent
// - on last line show the example with indent
//
StringBuffer sb = new StringBuffer();
sb.append("Usage:\n");
sb.append(" ");
sb.append(commandName);
sb.append(" [options]\n");
sb.append("Where options are:\n");
int len = 0;
for (IUiOption o: opts) {
int namelen = o.pname().length();
if ( o.sname() != null ) {
namelen += o.sname().length() + 3; // +1 for -, 1 for space, 1 for comma
}
len = Math.max(len, namelen);
}
String fmt1 = "%-" + (len) + "s"; // space for -- and another space
String fmt2 = "%-" + (len+3) + "s"; // A bit more for description and example lines
for (IUiOption o: opts) {
sb.append("--");
String cmd = o.pname();
if ( o.sname() != null ) {
cmd = cmd + ", -" + o.sname();
}
sb.append(String.format(fmt1, cmd));
if ( o.argname() != null ) {
sb.append(" <");
sb.append(o.argname());
sb.append(">");
}
if ( o.required() ) {
sb.append(" (required)");
}
if ( o.optargs() ) {
if ("true".equals(o.deflt())) {
sb.append(" (optional boolean)");
} else {
sb.append(" (optional)");
}
}
if ( o.noargs() ) {
sb.append(" (no arguments)");
}
if ( o.deflt() != null && !o.deflt().isEmpty()) {
String deflt = o.deflt();
if (deflt.startsWith("$$")) { // Lookup default in ducc.properties
deflt = DuccPropertiesResolver.get(deflt.substring(2));
}
sb.append("\n");
sb.append(String.format(fmt2, ""));
sb.append("Default: ");
sb.append(deflt);
}
if ( o.description() != null ) {
sb.append("\n");
sb.append(justify(len+3,o.description()));
}
if ( o.example() != null ) {
sb.append("\n");
sb.append(String.format(fmt2, ""));
sb.append("Example: ");
sb.append(o.example());
}
sb.append("\n\n");
}
return sb.toString();
}
public String toString()
{
StringBuffer sb = new StringBuffer();
for ( IUiOption k : option_to_value.keySet() ) {
sb.append(k.pname());
sb.append("=");
sb.append(option_to_value.get(k));
sb.append(" ");
}
return sb.toString();
}
/**
* Make sure the arguments make sense
*/
public void sanity()
{
/**
* If multiargs is true,
* noargs MUSt be false
* optargs MAY be true
* If noargs is true,
* multargs MUSt be false
* optargs MUSt be false
* default must be empty
* NOTE This is clumsy if the desired input comes from property files.
* The reason being, Properties extend HashTable which disallows
* null values (throws gratuitous NPE). So we'll make an
* assumption that noargs() options are implicitly boolean,
* and allow (but not require) any of the reasonable boolean
* representations as values for the default:
* true/false, t/f, yes/no, y/n, 1/0
* If optargs is true
* multargs MAY be true
* noargs MUST be false
* deflt must be non-null, non-empty-string
*/
boolean error = false;
StringBuffer sb = new StringBuffer();
for ( IUiOption o : opts ) {
if ( o.multiargs() && o.noargs() ) {
if ( error ) sb.append("\n");
sb.append("Option '" );
sb.append(o.pname());
sb.append("': multiargs() is true but noargs() is also true.");
error = true;
continue;
}
if ( o.noargs() && o.optargs() ) {
if ( error ) sb.append("\n");
sb.append("Option '" );
sb.append(o.pname());
sb.append("': optargs() is true but noargs() is also true.");
error = true;
continue;
}
if ( o.noargs() && o.deflt() != null ) {
try {
getBoolean(o); // if this doesn't throw the value is a boolean, which we allow
} catch ( IllegalArgumentException e ) {
if ( error ) sb.append("\n");
sb.append("Option '" );
sb.append(o.pname());
sb.append("': noargs() is true but a non-boolean default is defined.");
error = true;
}
}
if ( o.optargs() && ( o.deflt() == null ) ) {
if ( error ) sb.append("\n");
sb.append("Option '" );
sb.append(o.pname());
sb.append("': optargs() is true but no default is provided.");
error = true;
continue;
}
}
if ( error ) {
throw new IllegalArgumentException(sb.toString());
}
}
private void addProperties()
{
// precondition, caller must insure properties exists
//
// Similar to the parse loop, only no need to parse for -- strings, just get the (k, v) pair from the properties
//
for (String p : properties.stringPropertyNames()) {
IUiOption opt = name_to_option.get(p);
if ( opt == null ) {
throw new IllegalArgumentException("Illegal keyword for this command: " + p);
}
if ( ! contains(opt) ) { // proceed only if we don't have it
if ( opt.multiargs() ) {
// bop along to end, or next '-' collecting the arguments
//TODO - note, we don't have any multi-argument options in DUCC right now
throw new IllegalArgumentException("multiargs() is not yet implemented.");
} else {
String v = properties.getProperty(p);
if ( opt.noargs() ) { // must have no arguments
if ( v == null ) {
add(opt, null);
} else {
try {
getBoolean(opt); // throws if not boolean
add(opt, v);
} catch ( IllegalArgumentException e ) {
throw new IllegalArgumentException("Argument " + opt.pname() + ": no value allowed. Found " + v);
}
}
} else if ( opt.optargs() ) { // may or may not have optional arguments
// deal with optional argument
if ( v == null ) {
add(opt, opt.deflt()); // sanity checker insures deflt() is non-null
} else {
add(opt, v);
}
} else {
// Pick up a single argument, and next must start with '-', otherwise its an error.
if ( v == null ) {
// nope, required argument is missing
throw new IllegalArgumentException("Missing required value for argument " + opt.pname());
}
add(opt, v);
}
}
}
// else nothing, because the command-strings take precedence
}
}
private void addCommandLine()
{
int i = 0;
String k;
String v;
for ( i = 0; i < args.length; i++ ) {
if (args[i] == null) { // May have been removed by CliFixups
continue;
}
// constant: at the top of the loop we must be poised at the next '-' string
// must throw otherwise
if ( args[i].startsWith("--") ) {
k = args[i].substring(2);
} else if ( args[i].startsWith("-") ) {
k = args[i].substring(1);
} else {
throw new IllegalArgumentException("Unrecognized keyword: " + args[i]);
}
IUiOption opt = name_to_option.get(k);
if ( opt == null ) {
throw new IllegalArgumentException("Illegal keyword for this command: " + args[i]);
}
if ( opt.multiargs() ) {
// bop along to end, or next '-' collecting the arguments
//TODO - note, we don't have any multi-argument options in DUCC right now
throw new IllegalArgumentException("multiargs() is not yet implemented.");
} else {
if ( i+1 < args.length ) {
v = args[i+1];
} else {
v = null;
}
if ( opt.noargs() ) { // must have no arguments
if ( v == null || isOption(v) ) {
add(opt, null);
} else {
throw new IllegalArgumentException("Argument " + opt.pname() + ": no value allowed. Found " + v);
}
} else if ( opt.optargs() ) { // may or may not have optional arguments
// deal with optional argument
if ( v == null || isOption(v) ) {
add(opt, opt.deflt()); // sanity checker insures deflt() is non-null
} else {
add(opt, v);
i++;
}
} else {
// Pick up a single argument, and next must be an options, else it's an error
if ( v == null || isOption(v) ) {
// nope, required argument is missing
throw new IllegalArgumentException("Missing required value for argument " + opt.pname());
}
i++;
add(opt, v);
}
}
}
}
/**
* Final verification of the parsed command line.
*
* Checks that all required arguments are present.
*/
public void verify()
{
for (IUiOption o : opts) {
if ( o.required() && ! contains(o) ) {
throw new IllegalArgumentException("Missing required argument " + o.pname());
}
}
}
public void parse()
{
sanity();
if ( args != null ) addCommandLine();
if ( properties != null ) addProperties();
}
}