// ================================================================================================= // Copyright 2012 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.args; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import javax.annotation.Nullable; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.reflect.TypeToken; import com.twitter.common.args.apt.Configuration; import static com.google.common.base.Preconditions.checkArgument; /** * Description of a command line option/flag such as -foo=bar. * * @author Nick Kallen */ public class OptionInfo<T> extends ArgumentInfo<T> { static final String ARG_NAME_RE = "[\\w\\-\\.]+"; static final Pattern ARG_NAME_PATTERN = Pattern.compile(ARG_NAME_RE); static final String NEGATE_BOOLEAN = "no_"; private final String name; private final String prefix; public OptionInfo(String name, String help, Arg<T> arg, TypeToken<T> type, String prefix, List<Annotation> verifierAnnotations, @Nullable Class<? extends Parser<? extends T>> parser) { super(help, arg, type, verifierAnnotations, parser); this.name = name; this.prefix = Preconditions.checkNotNull(prefix); } static OptionInfo createFromField(Field field) { Preconditions.checkNotNull(field); CmdLine cmdLine = field.getAnnotation(CmdLine.class); if (cmdLine == null) { throw new Configuration.ConfigurationException( "No @CmdLine Arg annotation for field " + field); } @SuppressWarnings("unchecked") OptionInfo optionInfo = new OptionInfo( checkValidName(cmdLine.name()), cmdLine.help(), ArgumentInfo.getArgForField(field), TypeUtil.getTypeParamTypeToken(field), field.getDeclaringClass().getCanonicalName(), Arrays.asList(field.getAnnotations()), cmdLine.parser()); return optionInfo; } /** * Creates a new optioninfo. * * @param name Name of the option. * @param help Help string. * @param prefix Prefix scope for the option. * @param type Concrete option target type. * @param <T> Option type. * @return A new optioninfo. */ public static <T> OptionInfo<T> create(String name, String help, String prefix, Class<T> type) { return new OptionInfo<T>( checkValidName(name), help, Arg.<T>create(), TypeToken.of(type), prefix, ImmutableList.<Annotation>of(), null); } /** * The name of the optional parameter. This is used as a command-line optional argument, as in: * -name=value. */ public String getName() { return name; } String getNegatedName() { return NEGATE_BOOLEAN + this.name; } /** * Parse the value and store result in the Arg contained in this OptionInfo. */ void load(ParserOracle parserOracle, String optionName, String value) { Parser<? extends T> parser = getParser(parserOracle); Object result = parser.parse(parserOracle, getType().getType(), value); // [A] // If the arg type is boolean, check if the command line uses the negated boolean form. if (isBoolean()) { if (Predicates.in(Arrays.asList(getNegatedName(), getCanonicalNegatedName())) .apply(optionName)) { result = !(Boolean) result; // [B] } } // We know result is T at line [A] but throw this type information away to allow negation if T // is Boolean at line [B] @SuppressWarnings("unchecked") T parsed = (T) result; setValue(parsed); } /** * A fully-qualified name of a parameter. This is used as a command-line optional argument, as in: * -prefix.name=value. Prefix is typically a java package and class like * "com.twitter.myapp.MyClass". The difference between a canonical name and a regular name is that * it is in some circumstances for two names to collide; the canonical name, then, disambiguates. */ String getCanonicalName() { return this.prefix + "." + name; } boolean isBoolean() { return getType().getRawType() == Boolean.class; } /** * Similar to the canonical name, but with boolean arguments appends "no_", as in: * -com.twitter.common.MyApp.no_fire=false */ String getCanonicalNegatedName() { return this.prefix + "." + NEGATE_BOOLEAN + this.name; } private static String checkValidName(String name) { Preconditions.checkNotNull(name); checkArgument(!HELP_ARGS.contains(name), String.format("Argument name '%s' is reserved for builtin argument help", name)); checkArgument(ARG_NAME_PATTERN.matcher(name).matches(), String.format("Argument name '%s' does not match required pattern %s", name, ARG_NAME_RE)); return name; } }