All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jsimpledb.cli.ParamParser Maven / Gradle / Ivy

There is a newer version: 3.6.1
Show newest version

/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package org.jsimpledb.cli;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dellroad.stuff.java.Primitive;
import org.jsimpledb.parse.ParseException;
import org.jsimpledb.parse.ParseSession;
import org.jsimpledb.parse.ParseUtil;
import org.jsimpledb.parse.Parser;
import org.jsimpledb.parse.SpaceParser;
import org.jsimpledb.parse.WordParser;
import org.jsimpledb.util.ParseContext;

/**
 * Parses command line parameters, including optional flags, based on a specification string.
 *
 * 

* The specification string contains whitespace-separated parameter specifications; see {@link Param} for syntax. *

*/ public class ParamParser implements Parser> { private final LinkedHashSet optionFlags = new LinkedHashSet<>(); private final ArrayList params = new ArrayList<>(); public ParamParser(String spec) { if (spec.length() > 0) { for (String pspec : spec.split("\\s+")) { final Param param = new Param(pspec); if (param.isOption()) this.optionFlags.add(param); else this.params.add(param); } } } /** * Build a usage summary string. * * @param commandName command name * @return usage string */ public String getUsage(String commandName) { final StringBuilder buf = new StringBuilder(commandName); for (Param param : this.optionFlags) { buf.append(" [").append(param.getOptionFlag()); if (param.getTypeName() != null) buf.append(' ').append(param.getName()); buf.append(']'); } for (Param param : this.params) { buf.append(' '); if (param.getMin() == 0) buf.append('['); buf.append(param.getName()); if (param.getMax() > 1) buf.append(" ..."); if (param.getMin() == 0) buf.append(']'); } return buf.toString(); } /** * Get option flags. * * @return optional parameters */ public Set getOptionFlags() { return this.optionFlags; } /** * Get regular parameters, in expected order. * * @return mandatory parameters */ public List getParameters() { return this.params; } /** * Convert parameter spec type name into a {@link Parser}. * *

* The implementation in {@link ParamParser} supports the Java primitive types, and * {@code word} for a {@link String} containing one or more non-whitespace characters. * Subclasses should override as required to add additional supported types. *

* * @param typeName name of type * @return parser for parameters of the specified type * @throws IllegalArgumentException if {@code typeName} is unknown */ protected Parser getParser(String typeName) { Preconditions.checkArgument(typeName != null, "null typeName"); if (typeName.equals("word")) return new WordParser("parameter"); for (Primitive prim : Primitive.values()) { if (prim == Primitive.VOID) continue; if (typeName.equals(prim.getName())) return this.createPrimitiveParser(prim); } throw new IllegalArgumentException("unknown parameter type `" + typeName + "'"); } // This method exists solely to bind the generic type parameters private Parser createPrimitiveParser(final Primitive prim) { return new Parser() { @Override public T parse(ParseSession session, ParseContext ctx, boolean complete) { try { return prim.parseValue(ctx.matchPrefix("[^\\s]+").group()); } catch (IllegalArgumentException e) { throw new ParseException(ctx, "invalid " + prim.getName() + " value", e); } } }; } /** * Parse command line parameters. * * @param session associated session * @param ctx parse context positioned at whitespace preceeding parameters (if any) * @param complete true if we're only determining completions * @throws ParseException if parse fails */ @Override public Map parse(ParseSession session, ParseContext ctx, boolean complete) { // Store results here final HashMap values = new HashMap(); // First parse options boolean needSpace = !this.params.isEmpty() && this.params.get(0).getMin() > 0; while (true) { new SpaceParser(needSpace).parse(ctx, complete); needSpace = false; if (ctx.getInput().matches("(?s)^--([\\s;].*)?$")) { ctx.setIndex(ctx.getIndex() + 2); needSpace = !this.params.isEmpty() && this.params.get(0).getMin() > 0; break; } final Matcher matcher = ctx.tryPattern("(-[^\\s;]+)"); if (matcher == null) break; final String option = matcher.group(1); final Param param = Iterables.find(this.optionFlags, new Predicate() { @Override public boolean apply(Param param) { return option.equals(param.getOptionFlag()); } }, null); if (param == null) { throw new ParseException(ctx, "unrecognized option `" + option + "'").addCompletions( ParseUtil.complete(Iterables.transform(this.optionFlags, new Function() { @Override public String apply(Param param) { return param.getOptionFlag(); } }), option)); } final Parser parser = param.getParser(); if (parser != null) { new SpaceParser(true).parse(ctx, complete); values.put(param.getName(), parser.parse(session, ctx, complete)); } else values.put(param.getName(), true); needSpace = !this.params.isEmpty() && this.params.get(0).getMin() > 0; } // Next parse parameters for (Param param : this.params) { final ArrayList paramValues = new ArrayList<>(); final String typeName = param.getTypeName(); final Parser parser = param.getParser(); while (paramValues.size() < param.getMax()) { new SpaceParser(needSpace).parse(ctx, complete); needSpace = false; if (!ctx.getInput().matches("(?s)^[^\\s;].*$")) { if (complete) { parser.parse(session, new ParseContext(""), true); // calculate completions from empty string throw new ParseException(ctx, ""); // should never get here } break; } paramValues.add(parser.parse(session, ctx, complete)); needSpace = paramValues.size() < param.getMin(); } if (paramValues.size() < param.getMin()) { final ParseException e = new ParseException(ctx, "missing `" + param.getName() + "' parameter"); if (complete) { try { parser.parse(session, new ParseContext(""), true); } catch (ParseException e2) { e.addCompletions(e2.getCompletions()); } } throw e; } if (param.getMax() > 1) values.put(param.getName(), Arrays.asList(paramValues.toArray())); else if (!paramValues.isEmpty()) values.put(param.getName(), paramValues.get(0)); } // Check for trailing garbage new SpaceParser().parse(ctx, complete); if (!ctx.getInput().matches("(?s)^(;.*)?$")) throw new ParseException(ctx); // Done return values; } // Param /** * Represents one parsed parameter specification. * *

* {@link String} form is {@code -flag:name:type}, where the {@code -flag} is optional and indicates * an option flag, {@code name} is the name of the flag or parameter, and {@code type} is optional as well: * if missing, it indicates either an argumment-less option flag or a "word" type ({@link String} that is a * sequence of one or more non-whitespace characters). Otherwise {@code type} is the name of the parameter type. *

* *

* Non-option parameters may have a {@code ?} suffix if optional, * or a {@code +}, or {@code *} suffix if repeatable, in which case the result is a {@link List}. *

* *

* Spec string syntax examples: *

    *
  • -v:foo - boolean flag named {@code foo}
  • *
  • -v:foo:int - integer flag named {@code foo}
  • *
  • foo - string (word) parameter
  • *
  • foo:int - {@code int} parameter
  • *
  • foo? - optional final parameter
  • *
  • foo* - array of zero or more final parameters
  • *
  • foo+ - array of one or more final parameters
  • *
  • foo:int+ - array of one or more final {@code int} parameters
  • *
*/ public class Param { private final String optionFlag; private final String name; private final String typeName; private final Parser parser; private final int min; private final int max; public Param(String spec) { // Sanity check Preconditions.checkArgument(spec != null, "null spec"); // Apply pattern final Pattern pattern = Pattern.compile("((-[^\\s:]+):)?([^-][^\\s:?+*]*)(:([^\\s?+*]+))?([?+*])?"); final Matcher matcher = pattern.matcher(spec); if (!matcher.matches()) throw new IllegalArgumentException("invalid parameter spec `" + spec + "'"); // Get components this.optionFlag = matcher.group(2); this.name = matcher.group(3); this.typeName = matcher.group(5); final String repeat = matcher.group(6); if (repeat == null) { this.min = 1; this.max = 1; } else if (repeat.charAt(0) == '?') { this.min = 0; this.max = 1; } else if (repeat.charAt(0) == '*') { this.min = 0; this.max = Integer.MAX_VALUE; } else if (repeat.charAt(0) == '+') { this.min = 1; this.max = Integer.MAX_VALUE; } else throw new IllegalArgumentException("invalid parameter spec `" + spec + "'"); // Get parser this.parser = this.typeName != null ? ParamParser.this.getParser(typeName) : !this.isOption() ? new WordParser("parameter") : null; } public String getOptionFlag() { return this.optionFlag; } public boolean isOption() { return this.optionFlag != null; } public String getName() { return this.name; } public String getTypeName() { return this.typeName; } public int getMin() { return this.min; } public int getMax() { return this.max; } public Parser getParser() { return this.parser; } public String toString() { return (this.optionFlag != null ? this.optionFlag + ":" : "") + this.name + (this.typeName != null ? ":" + this.typeName : "") + (this.min == 0 && this.max == 1 ? "?" : this.min == 0 && this.max == Integer.MAX_VALUE ? "*" : this.min == 1 && this.max == Integer.MAX_VALUE ? "+" : ""); } } }