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

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

The newest version!

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

package org.jsimpledb.cli;

import com.google.common.base.Preconditions;

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.jsimpledb.core.FieldType;
import org.jsimpledb.core.FieldTypeRegistry;
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<>(); private final FieldTypeRegistry fieldTypeRegistry = new FieldTypeRegistry(); 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 all of the pre-defined types of {@link FieldTypeRegistry} * (identified by their names), plus {@code word} to parse 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"); final FieldType fieldType = this.fieldTypeRegistry.getFieldType(typeName); if (fieldType != null) { return (session, ctx, complete) -> { try { return fieldType.fromParseableString(ctx); } catch (IllegalArgumentException e) { throw new ParseException(ctx, "invalid " + fieldType.getName() + " value", e); } }; } throw new IllegalArgumentException("unknown parameter type `" + typeName + "'"); } /** * 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) { // Get next option 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; // Find matching Param final String option = matcher.group(1); final Param param = this.optionFlags.stream() .filter(p -> option.equals(p.getOptionFlag())) .findAny().orElseThrow(() -> new ParseException(ctx, "unrecognized option `" + option + "'") .addCompletions(ParseUtil.complete(this.optionFlags.stream().map(Param::getOptionFlag), option))); // Parse argument, if any 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 a parameter type * supported by {@link ParamParser#getParser ParamParser.getParser()}. * *

* 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; } @Override 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 ? "+" : ""); } } }