/* * Copyright (C) 2014 Facebook, Inc. * * 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 com.facebook.tools.parser; import com.facebook.tools.ErrorMessage; import java.io.PrintStream; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Verifies the command-line arguments match the {@link com.facebook.tools.parser.CliCommand}, and * makes it easy to extract arguments by name. */ public class CliParser { private final CliCommand command; private final Set<String> switches = new LinkedHashSet<>(); private final Map<String, String> values = new LinkedHashMap<>(); private final Map<String, List<String>> multiValues = new LinkedHashMap<>(); private final List<String> trailing = new ArrayList<>(); private final List<CliOption> missing = new ArrayList<>(); private final List<String> unexpected = new ArrayList<>(); private final List<Map.Entry<String, String>> duplicates = new ArrayList<>(); public CliParser(CliCommand command, List<String> arguments) { this.command = command; // getOption removes options as it parses them ArgumentList argumentList = new ArgumentList(arguments); for (CliOption option : command.getOptions()) { switches.addAll(option.getSwitchNames()); List<Map.Entry<String, String>> parsedValues = getOption(option, argumentList); if (parsedValues.isEmpty()) { if (option.isRequired()) { missing.add(option); } } else if (parsedValues.size() > 1) { if (option.isUnique()) { duplicates.addAll(parsedValues); } else { for (String switchName : option.getSwitchNames()) { for (Map.Entry<String, String> parsedValue : parsedValues) { List<String> switchValues = multiValues.get(switchName); if (switchValues == null) { switchValues = new ArrayList<>(); multiValues.put(switchName, switchValues); } switchValues.add(parsedValue.getValue()); } } } } else { String value = parsedValues.get(0).getValue(); for (String switchName : option.getSwitchNames()) { values.put(switchName, value); } } } Iterator<CliParameter> parameters = command.getParameters().iterator(); Iterator<String> trailingArguments = argumentList.trailing(); while (parameters.hasNext() && trailingArguments.hasNext()) { String name = parameters.next().getName(); values.put(name, trailingArguments.next()); trailingArguments.remove(); switches.add(name); } while (parameters.hasNext()) { CliParameter parameter = parameters.next(); switches.add(parameter.getName()); if (parameter.isRequired()) { CliOption option = new CliOption.SwitchBuilder() .withSwitch(parameter.getName()) .build(); missing.add(option); } } if (command.allowsTrailingParameter()) { while (trailingArguments.hasNext()) { trailing.add(trailingArguments.next()); trailingArguments.remove(); } } Iterator<String> unexpected = argumentList.remaining(); while (unexpected.hasNext()) { this.unexpected.add(unexpected.next()); } } public void verify(PrintStream out) { List<String> errors = new ArrayList<>(); if (!missing.isEmpty()) { StringBuilder missingMessage = new StringBuilder(80); List<String> missingSwitches = new ArrayList<>(missing.size()); for (CliOption option : missing) { missingSwitches.add(last(option.getSwitchNames())); } missingMessage.append("Missing required option"); if (missing.size() > 1) { missingMessage.append('s'); } missingMessage.append(": ").append(join(", ", missingSwitches)); errors.add(missingMessage.toString()); } if (!unexpected.isEmpty()) { String unexpectedMessage = "Unexpected parameters: " + join(" ", unexpected); errors.add(unexpectedMessage); } if (!duplicates.isEmpty()) { StringBuilder duplicatesMessage = new StringBuilder(80); List<String> duplicateSwitches = new ArrayList<>(duplicates.size()); for (Map.Entry<String, String> duplicate : duplicates) { duplicateSwitches.add(duplicate.getKey() + "=" + duplicate.getValue()); } duplicatesMessage.append("Duplicate options: ").append(join(", ", duplicateSwitches)); errors.add(duplicatesMessage.toString()); } if (!errors.isEmpty()) { out.println(command.getDocumentation()); out.println(); out.flush(); throw new ErrorMessage(join("\n", errors)); } } public String get(String option) { return get(option, CliConverter.STRING); } public <T> T get(String option, CliConverter<T> converter) { if (!switches.contains(option)) { throw new IllegalStateException( String.format("Expected option name to be one of %s, but got %s", switches, option) ); } String value = values.get(option); try { return converter.convert(value); } catch (Exception e) { throw new ErrorMessage(e, "Failed to parse %s %s", option, value); } } public List<String> getMulti(String option) { return getMulti(option, CliConverter.STRING); } public <T> List<T> getMulti(String option, CliConverter<T> converter) { List<String> values = multiValues.get(option); if (values == null || values.isEmpty()) { // value must have been specified less than twice T value = get(option, converter); return value == null ? Collections.<T>emptyList() : Collections.singletonList(value); } List<T> result = new ArrayList<>(); for (String value : values) { try { result.add(converter.convert(value)); } catch (Exception e) { throw new ErrorMessage(e, "Failed to parse %s %s", option, value); } } return result; } public List<String> getTrailing() { return trailing; } public <T> List<T> getTrailing(CliConverter<T> converter) { List<T> result = new ArrayList<>(); for (String value : trailing) { try { result.add(converter.convert(value)); } catch (Exception e) { throw new ErrorMessage(e, "Failed to parse %s", value); } } return result; } private static List<Map.Entry<String, String>> getOption( CliOption option, ArgumentList argumentList ) { List<Map.Entry<String, String>> result = new ArrayList<>(); boolean flag = option.isFlag(); for (String switchName : option.getSwitchNames()) { List<Map.Entry<String, String>> values; if (flag) { values = argumentList.removeFlag(switchName); } else { values = argumentList.removeSwitchValues(switchName); } result.addAll(values); } if (result.isEmpty()) { String defaultValue = option.getDefaultValue(); if (defaultValue != null) { Map.Entry<String, String> defaultEntry = new AbstractMap.SimpleImmutableEntry<>(last(option.getSwitchNames()), defaultValue); result.add(defaultEntry); } } return result; } private static String join(String separator, Iterable<?> values) { return join(separator, values.iterator()); } private static String join(String separator, Iterator<?> values) { StringBuilder result = new StringBuilder(80); while (values.hasNext()) { result.append(values.next()); if (values.hasNext()) { result.append(separator); } } return result.toString(); } private static <T> T last(Iterable<T> values) { return last(values.iterator()); } private static <T> T last(Iterator<T> values) { T result = null; while (values.hasNext()) { result = values.next(); } return result; } }