package edu.uw.zookeeper.common; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.SortedMap; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Lists; import com.google.common.collect.Iterators; import static com.google.common.base.Preconditions.*; /** * Simple command line argument parser. * * Only recognizes the format --NAME=VALUE or --NAME */ public class SimpleArguments implements Arguments { public static SimpleArguments defaults() { return create("", "", new String[0]); } public static SimpleArguments create(String programName, String description, String[] args) { return new SimpleArguments(ImmutableList.of(new SimpleOption(OPT_HELP)), programName, description, args); } protected static class SimpleOption implements Option { public static final String LONG_PREFIX = "--"; public static final String VALUE_SEP = "="; public static final String TRUE_VALUE = ""; private final String name; private final Optional<String> help; private final Optional<String> defaultValue; private Optional<String> value; public static String[] parse(String arg) { Splitter splitter = Splitter.on(SimpleOption.VALUE_SEP).limit(2) .trimResults(); if (arg.startsWith(SimpleOption.LONG_PREFIX)) { String theRest = arg.substring(SimpleOption.LONG_PREFIX .length()); checkArgument(theRest.length() > 0); String[] kv = Iterables.toArray(splitter.split(theRest), String.class); return kv; } return new String[0]; } public SimpleOption(String name) { this(name, Optional.<String> absent(), Optional.<String> absent()); } public SimpleOption(String name, String help) { this(name, Optional.of(help), Optional.<String> absent()); } public SimpleOption(String name, Optional<String> help, Optional<String> defaultValue) { this.name = checkNotNull(name); this.help = checkNotNull(help); this.defaultValue = checkNotNull(defaultValue); this.value = Optional.<String> absent(); } @Override public synchronized boolean hasValue() { return (value.isPresent() || defaultValue.isPresent()); } @Override public synchronized String getValue() { if (value.isPresent()) { return value.get(); } return defaultValue.get(); } @Override public synchronized void setValue(String value) { this.value = Optional.of(value); } @Override public String getName() { return name; } @Override public Optional<String> getHelp() { return help; } @Override public Optional<String> getDefaultValue() { return defaultValue; } @Override public String getUsage() { StringBuilder str = new StringBuilder(LONG_PREFIX).append(name); if (help.isPresent()) { str.append(VALUE_SEP).append(help.get()); } if (defaultValue.isPresent()) { str.append(' ').append(String.format("(%s)", defaultValue.get())); } return str.toString(); } @Override public synchronized String toString() { String value = hasValue() ? String.format("\"%s\"", getValue()) : ""; return String.format("%s=%s", name, value); } } protected static final String OPT_HELP = "help"; private final SortedMap<String, SimpleOption> options; private String programName; private String description; private String[] args; protected SimpleArguments(Iterable<SimpleOption> options, String programName, String description, String[] args) { // Maybe could try to auto-detect programName with // System.property(sun.java.command)? this.options = Maps.newTreeMap(); for (SimpleOption opt : checkNotNull(options)) { this.options.put(opt.getName(), opt); } this.programName = programName; this.description = description; this.args = args; } @Override public synchronized String getDescription() { return description; } @Override public synchronized SimpleArguments setDescription(String description) { this.description = checkNotNull(description); return this; } @Override public synchronized Option addOption(String name, Optional<String> help, Optional<String> defaultValue) { if (options.containsKey(name)) { SimpleOption existing = options.get(name); if (!existing.getHelp().equals(help) || !existing.getDefaultValue().equals(defaultValue)) { throw new IllegalArgumentException(name); } return existing; } else { SimpleOption option = new SimpleOption(name, help, defaultValue); options.put(name, option); return option; } } @Override public Option addOption(String name, String help) { return addOption(name, Optional.of(help), Optional.<String>absent()); } @Override public Option addOption(String name) { return addOption(name, Optional.<String>absent(), Optional.<String>absent()); } @Override public synchronized boolean has(String name) { checkNotNull(name); return options.containsKey(name); } @Override public synchronized boolean hasValue(String name) { checkArgument(name != null); Option opt = options.get(name); checkArgument(opt != null, name); return opt.hasValue(); } @Override public synchronized String getValue(String name) { checkArgument(name != null); Option opt = options.get(name); checkArgument(opt != null, name); return opt.getValue(); } @Override public synchronized String getUsage() { StringBuilder str = new StringBuilder(); str.append(String.format("Usage: %s ", programName)); Joiner joiner = Joiner.on(' ').skipNulls(); joiner.appendTo(str, Iterables.transform(options.values(), new Function<Option, String>() { @Override public String apply(Option opt) { return String.format("[%s]", opt.getUsage()); } })); str.append('\n'); if (! description.isEmpty()) { str.append('\n').append(description).append('\n'); } return str.toString(); } @Override public synchronized void parse() { setArgs(parse(args)); } @Override public void illegalValue(String name, String value) { throw new IllegalArgumentException(name + SimpleOption.VALUE_SEP + value); } @Override public boolean helpOptionSet() { return hasValue(OPT_HELP); } @Override public synchronized void setArgs(String[] args) { this.args = checkNotNull(args); } @Override public synchronized String[] getArgs() { return Arrays.copyOf(args, args.length); } @Override public synchronized String getProgramName() { return programName; } @Override public synchronized void setProgramName(String name) { this.programName = checkNotNull(name); } @Override public synchronized Iterator<Option> iterator() { return ImmutableList.<Option>copyOf(options.values()).iterator(); } @Override public synchronized String toString() { return MoreObjects.toStringHelper(Arguments.class) .add("args", Arrays.toString(args)) .add("options", Iterators.toString(iterator())) .toString(); } private String[] parse(String... args) { List<String> unknown = Lists.newArrayList(); for (String arg : args) { String[] kv = SimpleOption.parse(arg); if (kv.length == 0) { unknown.add(arg); continue; } assert (kv.length >= 1 && kv.length <= 2) : kv; String k = kv[0]; Option opt = options.get(k); if (opt == null) { unknown.add(arg); continue; } assert (opt != null) : opt; String value = (kv.length > 1 && kv[1].length() > 0) ? kv[1] : SimpleOption.TRUE_VALUE; opt.setValue(value); } return unknown.toArray(new String[0]); } }