de.carne.util.cmdline.CmdLineProcessor Maven / Gradle / Ivy
/*
* Copyright (c) 2016-2017 Holger de Carne and contributors, All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package de.carne.util.cmdline;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import de.carne.check.Nullable;
/**
* Utility class for command line processing.
*
* An instance of this class wraps a single command line.
*
* Actions can be added to define how the encountered arguments are processed. The following action types are supported:
*
* - switch (e.g. {@code --switch} or {@code -s}; see {@link #onSwitch(Consumer)})
* - option (e.g. {@code --option value} or {@code -o value}; see {@link #onOption(BiConsumer)})
* - unnamed (e.g. {@code file.txt)}; see {@link #onUnnamedOption(Consumer)})
* - unknown (default processing; see {@link #onUnknownArg(Consumer)})
*
* Invoking the {@link #process()} function processes all arguments and invokes the corresponding actions.
*/
public final class CmdLineProcessor {
private static final Pattern ACTION_ARG_PATTERN = Pattern.compile("(-[^-\\s])|(--[^-\\s]+)");
private final String cmd;
private final Iterable args;
private final List switchActions = new ArrayList<>();
private final List optionActions = new ArrayList<>();
@Nullable
private Consumer unnamedAction = null;
@Nullable
private Consumer unknownAction = null;
/**
* Construct {@link CmdLineProcessor}.
*
* @param cmd The command executing the command line (used by {@link #toString()} to build a complete command line
* string).
* @param args The command line to process.
*/
public CmdLineProcessor(String cmd, String[] args) {
this(cmd, Arrays.asList(args));
}
/**
* Construct {@link CmdLineProcessor}.
*
* @param cmd The command executing the command line.
* @param args The command line to process.
*/
public CmdLineProcessor(String cmd, Iterable args) {
this.cmd = cmd;
this.args = args;
}
/**
* Check whether an argument string is a valid action argument.
*
* An action argument must be of the form single '-' and character (e.g. {@code -a}) or double '-' and a name (e.g.
* {@code --argument}).
*
* @param arg The argument string to check.
* @return {@code true} if the argument string is a valid action argument.
*/
public static boolean isActionArg(String arg) {
return ACTION_ARG_PATTERN.matcher(arg).matches();
}
/**
* Process the command line and invoke the correspond actions.
*
* @throws CmdLineException if the command line contains an error.
* @see #onSwitch(Consumer)
* @see #onOption(BiConsumer)
* @see #onUnnamedOption(Consumer)
* @see #onUnknownArg(Consumer)
*/
public void process() throws CmdLineException {
ProcessingContext context = new ProcessingContext();
for (String arg : this.args) {
if (!context.processPendingOptionAction(arg) && !context.processOptionAction(arg, this.optionActions)
&& !context.processSwitchAction(arg, this.switchActions)) {
// No action found so far. Invoke the corresponding default action.
Consumer defaultAction = (isActionArg(arg) ? this.unknownAction : this.unnamedAction);
if (defaultAction != null) {
defaultAction.accept(arg);
} else {
throw new CmdLineException(this, arg);
}
}
}
context.verifyNoPendingOptionAction();
}
private class ProcessingContext {
@Nullable
private OptionCmdLineAction pendingOptionAction = null;
@Nullable
private String pendingArg = null;
ProcessingContext() {
// Nothing to do, just to make it accessible for out class
}
public boolean processPendingOptionAction(String option) throws CmdLineException {
// Check whether there is a pending named option waiting for completion.
// If this is the case, check and invoke the action.
OptionCmdLineAction optionAction = this.pendingOptionAction;
String arg = this.pendingArg;
boolean processed = false;
if (optionAction != null && arg != null) {
if (isActionArg(option)) {
throw new CmdLineException(CmdLineProcessor.this, arg);
}
optionAction.accept(this.pendingArg, option);
this.pendingOptionAction = null;
this.pendingArg = null;
processed = true;
}
return processed;
}
public void verifyNoPendingOptionAction() throws CmdLineException {
OptionCmdLineAction optionAction = this.pendingOptionAction;
String arg = this.pendingArg;
if (optionAction != null && arg != null) {
throw new CmdLineException(CmdLineProcessor.this, arg);
}
}
public boolean processOptionAction(String arg, List actions) {
// Check whether the argument is a known option argument.
// If this is the case, remember it as pending and continue (processing will be done above).
Optional optOptionAction = actions.stream().filter(action -> action.contains(arg))
.findFirst();
boolean processed = false;
if (optOptionAction.isPresent()) {
this.pendingOptionAction = optOptionAction.get();
this.pendingArg = arg;
processed = true;
}
return processed;
}
public boolean processSwitchAction(String arg, List actions) {
// Check whether the argument is a known switch argument.
// If this is the case, invoke it.
Optional optSwitchAction = actions.stream().filter(action -> action.contains(arg))
.findFirst();
boolean processed = false;
if (optSwitchAction.isPresent()) {
optSwitchAction.get().accept(arg);
processed = true;
}
return processed;
}
}
/**
* Add a {@link CmdLineAction} for switch argument ({@code e.g. --switch}) processing.
*
* @param action The {@link Consumer} to invoke with the argument string.
* @return The created {@link CmdLineAction}.
*/
public CmdLineAction onSwitch(Consumer action) {
SwitchCmdLineAction switchAction = new SwitchCmdLineAction(action);
this.switchActions.add(switchAction);
return switchAction;
}
/**
* Add a {@link CmdLineAction} for option argument ({@code e.g. --option value}) processing.
*
* @param action The {@link BiConsumer} to invoke with the argument and option string.
* @return The created {@link CmdLineAction}.
*/
public CmdLineAction onOption(BiConsumer action) {
OptionCmdLineAction optionAction = new OptionCmdLineAction(action);
this.optionActions.add(optionAction);
return optionAction;
}
/**
* Add an action for unnamed options (e.g. {@code file.txt}).
*
* If no action is defined for unnamed options the command line processing will fail in case an unnamed option is
* encountered.
*
* @param action The {@link Consumer} to invoke with the option string.
*/
public void onUnnamedOption(Consumer action) {
this.unnamedAction = action;
}
/**
* Add an action for unknown arguments (e.g. {@code --unknown}).
*
* If no action is defined for unknown arguments the command line processing will fail in case an unknown arguments
* is encountered.
*
* @param action The {@link Consumer} to invoke with the argument string.
*/
public void onUnknownArg(Consumer action) {
this.unknownAction = action;
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append(this.cmd);
for (String arg : this.args) {
buffer.append(" ");
buffer.append(arg);
}
return buffer.toString();
}
private class SwitchCmdLineAction extends CmdLineAction implements Consumer {
private final Consumer action;
SwitchCmdLineAction(Consumer action) {
this.action = action;
}
@Override
public void accept(@Nullable String t) {
this.action.accept(t);
}
}
private class OptionCmdLineAction extends CmdLineAction implements BiConsumer {
private final BiConsumer action;
OptionCmdLineAction(BiConsumer action) {
this.action = action;
}
@Override
public void accept(@Nullable String t, @Nullable String u) {
this.action.accept(t, u);
}
}
}