se.softhouse.jargo.CommandLineParserInstance.java Source code

Java tutorial

Introduction

Here is the source code for se.softhouse.jargo.CommandLineParserInstance.java

Source

/* Copyright 2013 Jonatan Jnsson
 *
 *    Licensed 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 se.softhouse.jargo;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newHashMapWithExpectedSize;
import static com.google.common.collect.Sets.newHashSetWithExpectedSize;
import static com.google.common.collect.Sets.newLinkedHashSetWithExpectedSize;
import static java.util.Collections.emptySet;
import static se.softhouse.common.guavaextensions.Preconditions2.checkNulls;
import static se.softhouse.common.strings.Describables.format;
import static se.softhouse.common.strings.StringsUtil.NEWLINE;
import static se.softhouse.common.strings.StringsUtil.TAB;
import static se.softhouse.common.strings.StringsUtil.startsWithAndHasMore;
import static se.softhouse.jargo.Argument.IS_OF_VARIABLE_ARITY;
import static se.softhouse.jargo.Argument.IS_REQUIRED;
import static se.softhouse.jargo.Argument.ParameterArity.NO_ARGUMENTS;
import static se.softhouse.jargo.ArgumentBuilder.DEFAULT_SEPARATOR;
import static se.softhouse.jargo.ArgumentExceptions.forMissingArguments;
import static se.softhouse.jargo.ArgumentExceptions.forUnallowedRepetitionArgument;
import static se.softhouse.jargo.ArgumentExceptions.withMessage;
import static se.softhouse.jargo.ArgumentExceptions.wrapException;
import static se.softhouse.jargo.CommandLineParser.US_BY_DEFAULT;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

import se.softhouse.common.collections.CharacterTrie;
import se.softhouse.common.strings.StringsUtil;
import se.softhouse.jargo.ArgumentExceptions.UnexpectedArgumentException;
import se.softhouse.jargo.StringParsers.InternalStringParser;
import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
import se.softhouse.jargo.internal.Texts.UsageTexts;
import se.softhouse.jargo.internal.Texts.UserErrors;

import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.UnmodifiableIterator;
import com.google.common.io.Files;

/**
 * A snapshot view of a {@link CommandLineParser} configuration.
 */
@Immutable
final class CommandLineParserInstance {
    /**
     * A list where arguments created without names is put
     */
    @Nonnull
    private final List<Argument<?>> indexedArguments;

    /**
     * A map containing short-named arguments, long-named arguments and named arguments that ignore
     * case
     */
    @Nonnull
    private final NamedArguments namedArguments;

    /**
     * Stores arguments that either has a special {@link ArgumentBuilder#separator()}, or is a
     * {@link ArgumentBuilder#asPropertyMap()}. Handles {@link ArgumentBuilder#ignoreCase()} as
     * well.
     */
    @Nonnull
    private final SpecialArguments specialArguments;

    /**
     * Stored separately (as well) as {@link Command}s need access to them through the
     * {@link ArgumentIterator}. This avoids traversing all named arguments for each parse
     * checking for help argument definitions
     */
    @Nonnull
    private final Map<String, Argument<?>> helpArguments;

    @Nonnull
    private final Set<Argument<?>> allArguments;

    /**
     * Used by {@link Command} to indicate that this parser is part of a {@link Command}
     */
    private final boolean isCommandParser;

    private final ProgramInformation programInformation;

    private final Locale locale;

    CommandLineParserInstance(Iterable<Argument<?>> argumentDefinitions, ProgramInformation information,
            Locale locale, boolean isCommandParser) {
        int nrOfArgumentsToHandle = Iterables.size(argumentDefinitions);
        this.indexedArguments = newArrayListWithCapacity(nrOfArgumentsToHandle);
        this.namedArguments = new NamedArguments(nrOfArgumentsToHandle);
        this.specialArguments = new SpecialArguments();
        this.helpArguments = newHashMap();
        this.allArguments = newLinkedHashSetWithExpectedSize(nrOfArgumentsToHandle);

        this.programInformation = information;
        this.locale = locale;
        this.isCommandParser = isCommandParser;
        for (Argument<?> definition : argumentDefinitions) {
            addArgumentDefinition(definition);
        }
        verifyThatIndexedAndRequiredArgumentsWasGivenBeforeAnyOptionalArguments();
        verifyUniqueMetasForRequiredAndIndexedArguments();
        verifyThatOnlyOneArgumentIsOfVariableArity();
    }

    CommandLineParserInstance(Iterable<Argument<?>> argumentDefinitions) {
        this(argumentDefinitions, ProgramInformation.AUTO, US_BY_DEFAULT, false);
    }

    private void addArgumentDefinition(final Argument<?> definition) {
        if (definition.isIndexed()) {
            indexedArguments.add(definition);
        } else {
            for (String name : definition.names()) {
                addNamedArgumentDefinition(name, definition);
            }
        }
        boolean added = allArguments().add(definition);
        checkArgument(added, ProgrammaticErrors.UNIQUE_ARGUMENT, definition);
    }

    private void addNamedArgumentDefinition(final String name, final Argument<?> definition) {
        Argument<?> oldDefinition = null;
        String separator = definition.separator();
        if (definition.isPropertyMap()) {
            oldDefinition = specialArguments.put(name, definition);
        } else if (separator.equals(DEFAULT_SEPARATOR)) {
            oldDefinition = namedArguments.put(name, definition);
        } else {
            oldDefinition = specialArguments.put(name + separator, definition);
        }
        if (definition.isHelpArgument()) {
            helpArguments.put(name, definition);
        }
        checkArgument(oldDefinition == null, ProgrammaticErrors.NAME_COLLISION, name);
    }

    /**
     * Specifying the optional argument before the required argument would make the optional
     * argument required
     */
    private void verifyThatIndexedAndRequiredArgumentsWasGivenBeforeAnyOptionalArguments() {
        int lastRequiredIndexedArgument = 0;
        int firstOptionalIndexedArgument = Integer.MAX_VALUE;
        for (int i = 0; i < indexedArguments.size(); i++) {
            Argument<?> indexedArgument = indexedArguments.get(i);
            if (indexedArgument.isRequired()) {
                lastRequiredIndexedArgument = i;
            } else if (firstOptionalIndexedArgument == Integer.MAX_VALUE) {
                firstOptionalIndexedArgument = i;
            }
        }
        checkArgument(lastRequiredIndexedArgument <= firstOptionalIndexedArgument,
                ProgrammaticErrors.REQUIRED_ARGUMENTS_BEFORE_OPTIONAL, firstOptionalIndexedArgument,
                lastRequiredIndexedArgument);
    }

    /**
     * Otherwise the error texts becomes ambiguous
     */
    private void verifyUniqueMetasForRequiredAndIndexedArguments() {
        Set<String> metasForRequiredAndIndexedArguments = newHashSetWithExpectedSize(indexedArguments.size());
        for (Argument<?> indexedArgument : filter(indexedArguments, IS_REQUIRED)) {
            String meta = indexedArgument.metaDescriptionInRightColumn();
            boolean metaWasUnique = metasForRequiredAndIndexedArguments.add(meta);
            checkArgument(metaWasUnique, ProgrammaticErrors.UNIQUE_METAS, meta);
        }
    }

    /**
     * How would one know when the first argument considers itself satisfied?
     */
    private void verifyThatOnlyOneArgumentIsOfVariableArity() {
        Collection<Argument<?>> indexedVariableArityArguments = filter(indexedArguments, IS_OF_VARIABLE_ARITY);
        checkArgument(indexedVariableArityArguments.size() <= 1, ProgrammaticErrors.SEVERAL_VARIABLE_ARITY_PARSERS,
                indexedVariableArityArguments);
    }

    @Nonnull
    ParsedArguments parse(final Iterable<String> actualArguments) throws ArgumentException {
        ArgumentIterator arguments = ArgumentIterator.forArguments(actualArguments, helpArguments);
        return parse(arguments, locale());
    }

    @Nonnull
    ParsedArguments parse(ArgumentIterator arguments, Locale inLocale) throws ArgumentException {
        ParsedArguments holder = parseArguments(arguments, inLocale);

        Collection<Argument<?>> missingArguments = holder.requiredArgumentsLeft();
        if (missingArguments.size() > 0)
            throw forMissingArguments(missingArguments).withUsage(usage(inLocale));

        for (Argument<?> arg : holder.parsedArguments()) {
            holder.finalize(arg);
            limitArgument(arg, holder, inLocale);
        }
        if (!isCommandParser()) {
            arguments.executeLastCommand();
        }
        return holder;
    }

    private ParsedArguments parseArguments(final ArgumentIterator iterator, Locale inLocale)
            throws ArgumentException {
        ParsedArguments holder = new ParsedArguments(allArguments());
        iterator.setCurrentParser(this);
        while (iterator.hasNext()) {
            Argument<?> definition = null;
            try {
                iterator.setCurrentArgumentName(iterator.next());
                definition = getDefinitionForCurrentArgument(iterator, holder);
                if (definition == null) {
                    break;
                }
                parseArgument(iterator, holder, definition, inLocale);
            } catch (ArgumentException e) {
                e.withUsedArgumentName(iterator.getCurrentArgumentName());
                if (definition != null) {
                    e.withUsageReference(definition);
                }
                throw e.withUsage(usage(inLocale));
            }
        }
        return holder;
    }

    private <T> void parseArgument(final ArgumentIterator arguments, final ParsedArguments parsedArguments,
            final Argument<T> definition, Locale inLocale) throws ArgumentException {
        if (parsedArguments.wasGiven(definition) && !definition.isAllowedToRepeat() && !definition.isPropertyMap())
            throw forUnallowedRepetitionArgument(arguments.current());

        InternalStringParser<T> parser = definition.parser();
        T oldValue = parsedArguments.getValue(definition);

        T parsedValue = parser.parse(arguments, oldValue, definition, inLocale);
        parsedArguments.put(definition, parsedValue);
    }

    /**
     * @return a definition that defines how to handle the current argument
     * @throws UnexpectedArgumentException if no definition could be found
     *             for the current argument
     */
    @Nullable
    private Argument<?> getDefinitionForCurrentArgument(final ArgumentIterator iterator,
            final ParsedArguments holder) throws ArgumentException {
        if (iterator.allowsOptions()) {
            Argument<?> byName = lookupByName(iterator);
            if (byName != null)
                return byName;

            Argument<?> option = batchOfShortNamedArguments(iterator, holder);
            if (option != null)
                return option;
        }

        Argument<?> indexedArgument = indexedArgument(iterator, holder);
        if (indexedArgument != null)
            return indexedArgument;

        if (isCommandParser()) {
            // Rolling back here means that the parent parser/command will receive the argument
            // instead, maybe it can handle it
            iterator.previous();
            return null;
        }

        guessAndSuggestIfCloseMatch(iterator, holder);

        // We're out of order, tell the user what we didn't like
        throw ArgumentExceptions.forUnexpectedArgument(iterator);
    }

    /**
     * Looks for {@link ArgumentBuilder#names(String...) named arguments}
     */
    private Argument<?> lookupByName(ArgumentIterator arguments) {
        String currentArgument = arguments.current();
        // Ordinary, named, arguments that directly matches the argument
        Argument<?> definition = namedArguments.get(currentArgument);
        if (definition != null)
            return definition;

        // Property Maps,Special separator, ignore case arguments
        Entry<String, Argument<?>> entry = specialArguments.get(currentArgument);
        if (entry != null) {
            // Remove "-D" from "-Dkey=value"
            String valueAfterSeparator = currentArgument.substring(entry.getKey().length());
            arguments.setNextArgumentTo(valueAfterSeparator);
            return entry.getValue();
        }
        definition = arguments.helpArgument(currentArgument);
        return definition;
    }

    /**
     * Batch of short-named optional arguments
     * For instance, "-fs" was used instead of "-f -s"
     */
    private Argument<?> batchOfShortNamedArguments(ArgumentIterator arguments, ParsedArguments holder)
            throws ArgumentException {
        String currentArgument = arguments.current();
        if (startsWithAndHasMore(currentArgument, "-")) {
            List<Character> givenCharacters = Lists.charactersOf(currentArgument.substring(1));
            Set<Argument<?>> foundOptions = newLinkedHashSetWithExpectedSize(givenCharacters.size());
            Argument<?> lastOption = null;
            for (Character optionName : givenCharacters) {
                lastOption = namedArguments.get("-" + optionName);
                if (lastOption == null || lastOption.parser().parameterArity() != NO_ARGUMENTS
                        || !foundOptions.add(lastOption)) {
                    // Abort as soon as an unexpected character is discovered
                    break;
                }
            }
            // Only accept the argument if all characters could be matched and no duplicate
            // characters were found
            if (foundOptions.size() == givenCharacters.size()) {
                // The last option is handled with the return
                foundOptions.remove(lastOption);

                // A little ugly that this get method has side-effects but the alternative is to
                // return a list of arguments to parse and as this should be a rare case it's not
                // worth it, the result is the same at least
                for (Argument<?> option : foundOptions) {
                    parseArgument(arguments, holder, option, null);
                }
                return lastOption;
            }
        }
        return null;
    }

    /**
     * Looks for {@link ArgumentBuilder#names(String...) indexed arguments}
     */
    private Argument<?> indexedArgument(ArgumentIterator arguments, ParsedArguments holder) {
        if (holder.indexedArgumentsParsed() < indexedArguments.size()) {
            Argument<?> definition = indexedArguments.get(holder.indexedArgumentsParsed());
            arguments.previous();
            // This helps the error messages explain which of the indexed arguments that failed
            if (isCommandParser()) {
                arguments.setCurrentArgumentName(arguments.usedCommandName());
            } else {
                arguments.setCurrentArgumentName(definition.metaDescriptionInRightColumn());
            }
            return definition;
        }
        return null;
    }

    /**
     * Suggests probable, valid, alternatives for a faulty argument, based on the
     * {@link StringsUtil#levenshteinDistance(String, String)}
     */
    private void guessAndSuggestIfCloseMatch(ArgumentIterator arguments, ParsedArguments holder)
            throws ArgumentException {
        Set<String> availableArguments = Sets.union(holder.nonParsedArguments(), arguments.nonParsedArguments());

        if (!availableArguments.isEmpty()) {
            List<String> suggestions = StringsUtil.closestMatches(arguments.current(), availableArguments,
                    ONLY_REALLY_CLOSE_MATCHES);
            if (!suggestions.isEmpty())
                throw withMessage(
                        format(UserErrors.SUGGESTION, arguments.current(), NEW_LINE_AND_TAB.join(suggestions)));
        }
    }

    private static final int ONLY_REALLY_CLOSE_MATCHES = 4;
    private static final Joiner NEW_LINE_AND_TAB = Joiner.on(NEWLINE + TAB);

    private <T> void limitArgument(@Nonnull Argument<T> arg, ParsedArguments holder, Locale inLocale)
            throws ArgumentException {
        try {
            arg.checkLimit(holder.getValue(arg));
        } catch (IllegalArgumentException e) {
            throw wrapException(e).withUsageReference(arg).withUsedArgumentName(arg.toString())
                    .withUsage(usage(inLocale));
        } catch (ArgumentException e) {
            throw e.withUsageReference(arg).withUsage(usage(inLocale));
        }
    }

    /**
     * Returns <code>true</code> if this is a parser for a specific {@link Command}
     */
    boolean isCommandParser() {
        return isCommandParser;
    }

    Set<Argument<?>> allArguments() {
        return allArguments;
    }

    ProgramInformation programInformation() {
        return programInformation;
    }

    Locale locale() {
        return locale;
    }

    @CheckReturnValue
    @Nonnull
    Usage usage(Locale inLocale) {
        return new Usage(allArguments(), inLocale, programInformation(), isCommandParser());
    }

    @CheckReturnValue
    @Nonnull
    ArgumentException helpFor(ArgumentIterator arguments, Locale inLocale) throws ArgumentException {
        ArgumentException e = withMessage("Help requested with " + arguments.current());
        Usage usage = null;
        if (arguments.hasNext()) {
            arguments.next();
            Argument<?> argument = lookupByName(arguments);
            if (argument == null)
                throw withMessage(format(UserErrors.UNKNOWN_ARGUMENT, arguments.current()));
            usage = new Usage(Arrays.<Argument<?>>asList(argument), inLocale, programInformation(),
                    isCommandParser());
            if (isCommandParser()) {
                String withCommandReference = ". Usage for " + argument + " (argument to "
                        + arguments.usedCommandName() + "):";
                e.withUsageReference(withCommandReference);
            } else {
                e.withUsageReference(argument);
            }
        } else {
            usage = usage(inLocale);
            if (isCommandParser()) {
                e.withUsageReference(". See usage for " + arguments.usedCommandName() + " below:");
            } else {
                e.withUsageReference(". See usage below:");
            }
        }
        return e.withUsage(usage);
    }

    @Override
    public String toString() {
        return usage(locale()).toString();
    }

    /**
     * Wraps a list of given arguments and remembers
     * which argument that is currently being parsed. Plays a key role in making
     * {@link CommandLineParserInstance} {@link ThreadSafe} as it holds the current state of a parse
     * invocation.
     */
    @NotThreadSafe
    static final class ArgumentIterator extends UnmodifiableIterator<String> {
        private final List<String> arguments;

        /**
         * Corresponds to one of the {@link Argument#names()} that has been given from the command
         * line. This is updated as soon as the parsing of a new argument begins.
         * For indexed arguments this will be the meta description instead.
         */
        private String currentArgumentName;
        private int currentArgumentIndex;
        private boolean endOfOptionsReceived;

        private int indexOfLastCommand = -1;
        private Command lastCommandParsed;
        private ParsedArguments argumentsToLastCommand;

        /**
         * In case of {@link Command}s this may be the parser for a specific {@link Command} or just
         * simply the main parser
         */
        private CommandLineParserInstance currentParser;
        private final Map<String, Argument<?>> helpArguments;

        /**
         * @param actualArguments a list of arguments, will be modified
         */
        private ArgumentIterator(Iterable<String> actualArguments, Map<String, Argument<?>> helpArguments) {
            this.arguments = checkNulls(actualArguments, "Argument strings may not be null");
            this.helpArguments = helpArguments;
        }

        Argument<?> helpArgument(String currentArgument) {
            return helpArguments.get(currentArgument);
        }

        /**
         * Returns <code>true</code> if {@link UsageTexts#END_OF_OPTIONS} hasn't been received yet.
         */
        boolean allowsOptions() {
            return !endOfOptionsReceived;
        }

        void setCurrentParser(CommandLineParserInstance instance) {
            currentParser = instance;
        }

        void rememberAsCommand() {
            executeLastCommand();
            // The command has moved the index by 1 therefore the -1 to get the index of the
            // commandName
            indexOfLastCommand = currentArgumentIndex - 1;
        }

        void rememberInvocationOfCommand(Command command, ParsedArguments argumentsToCommand) {
            executeLastCommand();
            lastCommandParsed = command;
            this.argumentsToLastCommand = argumentsToCommand;
        }

        void executeLastCommand() {
            if (lastCommandParsed != null) {
                lastCommandParsed.execute(argumentsToLastCommand);
                lastCommandParsed = null;
            }
        }

        /**
         * Returns any non-parsed arguments to the last command that was executed
         */
        Set<String> nonParsedArguments() {
            if (lastCommandParsed != null)
                return argumentsToLastCommand.nonParsedArguments();
            return emptySet();
        }

        /**
         * For indexed arguments in commands the used command name is returned so that when
         * multiple commands (or multiple command names) are used it's clear which command the
         * offending argument is part of
         */
        String usedCommandName() {
            return arguments.get(indexOfLastCommand);
        }

        static ArgumentIterator forArguments(Iterable<String> arguments, Map<String, Argument<?>> helpArguments) {
            return new ArgumentIterator(arguments, helpArguments);
        }

        static ArgumentIterator forArguments(Iterable<String> arguments) {
            return new ArgumentIterator(arguments, Collections.<String, Argument<?>>emptyMap());
        }

        /**
         * Returns the string that was given by the previous {@link #next()} invocation.
         */
        String current() {
            return arguments.get(currentArgumentIndex - 1);
        }

        @Override
        public boolean hasNext() {
            return currentArgumentIndex < arguments.size();
        }

        @Override
        public String next() {
            String nextArgument = arguments.get(currentArgumentIndex++);
            nextArgument = skipAheadIfEndOfOptions(nextArgument);
            nextArgument = readArgumentsFromFile(nextArgument);

            return nextArgument;
        }

        /**
         * Skips {@link UsageTexts#END_OF_OPTIONS} if the parser hasn't received it yet.
         * This is to allow the string {@link UsageTexts#END_OF_OPTIONS} as an indexed argument
         * itself.
         */
        private String skipAheadIfEndOfOptions(String nextArgument) {
            if (!endOfOptionsReceived && nextArgument.equals(UsageTexts.END_OF_OPTIONS)) {
                endOfOptionsReceived = true;
                return next();
            }
            return nextArgument;
        }

        /**
         * Reads arguments from files if the argument starts with a
         * {@link UsageTexts#FILE_REFERENCE_PREFIX}.
         */
        private String readArgumentsFromFile(String nextArgument) {
            // TODO(jontejj): add possibility to disable this feature? It has some security
            // implications as the caller can input any files and if this parser was exposed from a
            // server...
            if (nextArgument.startsWith(UsageTexts.FILE_REFERENCE_PREFIX)) {
                String filename = nextArgument.substring(1);
                File fileWithArguments = new File(filename);
                if (fileWithArguments.exists()) {
                    try {
                        appendArgumentsAtCurrentPosition(Files.readLines(fileWithArguments, Charsets.UTF_8));
                    } catch (IOException errorWhileReadingFile) {
                        throw withMessage("Failed while reading arguments from: " + filename,
                                errorWhileReadingFile);
                    }
                    // Recursive call adds support for file references from within the file itself
                    return next();
                }
            }
            return nextArgument;
        }

        private void appendArgumentsAtCurrentPosition(List<String> argumentsToAppend) {
            arguments.addAll(currentArgumentIndex, argumentsToAppend);
        }

        @Override
        public String toString() {
            return arguments.subList(currentArgumentIndex, arguments.size()).toString();
        }

        /**
         * The opposite of {@link #next()}. In short it makes this iterator return what
         * {@link #next()} returned last time once again.
         * 
         * @return the {@link #current()} argument
         */
        String previous() {
            return arguments.get(--currentArgumentIndex);
        }

        int nrOfRemainingArguments() {
            return arguments.size() - currentArgumentIndex;
        }

        void setNextArgumentTo(String newNextArgumentString) {
            arguments.set(--currentArgumentIndex, newNextArgumentString);
        }

        boolean hasPrevious() {
            return currentArgumentIndex > 0;
        }

        void setCurrentArgumentName(String argumentName) {
            currentArgumentName = argumentName;
        }

        String getCurrentArgumentName() {
            return currentArgumentName;
        }

        CommandLineParserInstance currentParser() {
            return currentParser;
        }
    }

    private static final class NamedArguments {
        private final Map<String, Argument<?>> namedArguments;

        NamedArguments(int expectedSize) {
            namedArguments = newHashMapWithExpectedSize(expectedSize);
        }

        Argument<?> put(String name, Argument<?> definition) {
            if (definition.isIgnoringCase())
                return namedArguments.put(name.toLowerCase(Locale.ENGLISH), definition);
            return namedArguments.put(name, definition);
        }

        Argument<?> get(String name) {
            Argument<?> definition = namedArguments.get(name);
            if (definition != null)
                return definition;

            String lowerCase = name.toLowerCase(Locale.ENGLISH);
            definition = namedArguments.get(lowerCase);
            if (definition != null && definition.isIgnoringCase())
                return definition;
            return null;
        }
    }

    private static final class SpecialArguments {
        private final CharacterTrie<Argument<?>> specialArguments;

        SpecialArguments() {
            specialArguments = CharacterTrie.newTrie();
        }

        Argument<?> put(String name, Argument<?> definition) {
            if (definition.isIgnoringCase())
                return specialArguments.put(name.toLowerCase(Locale.ENGLISH), definition);
            return specialArguments.put(name, definition);
        }

        Entry<String, Argument<?>> get(String name) {
            Entry<String, Argument<?>> entry = specialArguments.findLongestPrefix(name);
            if (entry != null)
                return entry;

            String lowerCase = name.toLowerCase(Locale.ENGLISH);
            entry = specialArguments.findLongestPrefix(lowerCase);
            if (entry != null && entry.getValue().isIgnoringCase())
                return entry;
            return null;
        }
    }
}