blob: c264b4977810c241f54a70da498debba99f2aa98 [file] [log] [blame] [edit]
/*
* libbrlapi - A library providing access to braille terminals for applications.
*
* Copyright (C) 2006-2023 by
* Samuel Thibault <Samuel.Thibault@ens-lyon.org>
* Sébastien Hinderer <Sebastien.Hinderer@ens-lyon.org>
*
* libbrlapi comes with ABSOLUTELY NO WARRANTY.
*
* This is free software, placed under the terms of the
* GNU Lesser General Public License, as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any
* later version. Please see the file LICENSE-LGPL for details.
*
* Web Page: http://brltty.app/
*
* This software is maintained by Dave Mielke <dave@mielke.cc>.
*/
package org.a11y.brlapi;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
public abstract class Program extends ProgramComponent implements Runnable {
protected abstract void runProgram () throws ProgramException;
private String programName = null;
public final boolean isClient () {
return isClient(this);
}
public final String getProgramName() {
if (programName == null) return getObjectName();
return programName;
}
public final Program setProgramName(String name) {
programName = name;
return this;
}
protected final void writeProgramMessage (String format, Object... arguments) {
System.err.println((getObjectName() + ": " + String.format(format, arguments)));
}
protected static class Option {
public final static char PREFIX_CHARACTER = '-';
public interface Handler {
public void handleOption (String[] operands) throws SyntaxException;
}
private final String optionName;
private final Handler optionHandler;
private final String[] operandDescriptions;
public Option (String name, Handler handler, String... operands) {
optionName = name;
optionHandler = handler;
operandDescriptions = operands;
}
public final String getName () {
return optionName;
}
public final Handler getHandler () {
return optionHandler;
}
public final String[] getOperands () {
int count = operandDescriptions.length;
String[] result = new String[count];
System.arraycopy(operandDescriptions, 0, result, 0, count);
return result;
}
}
private final String[] programArguments;
private final KeywordMap<Option> programOptions = new KeywordMap<>();
protected final void addOption (String name, Option.Handler handler, String... operands) {
programOptions.put(name, new Option(name, handler, operands));
}
private List<String> requiredParameters = null;
private List<String> optionalParameters = null;
private boolean haveRepeatingParameter = false;
protected final void addRequiredParameters (String... parameters) {
if (optionalParameters != null) {
throw new IllegalStateException("optional parameters already added");
}
if (requiredParameters == null) {
requiredParameters = new ArrayList<>();
}
for (String parameter : parameters) {
requiredParameters.add(parameter);
}
}
protected final void addOptionalParameters (String... parameters) {
if (haveRepeatingParameter) {
throw new IllegalStateException("repeating parameter already added");
}
if (optionalParameters == null) {
optionalParameters = new ArrayList<>();
}
for (String parameter : parameters) {
optionalParameters.add(parameter);
}
}
protected final void addRepeatingParameter (String parameter) {
addOptionalParameters(parameter);
haveRepeatingParameter = true;
}
public String getPurpose() {
return null;
}
protected void extendUsageSummary(StringBuilder usage) {}
public final static char USAGE_OPTIONAL_BEGIN = '[';
public final static char USAGE_OPTIONAL_END = ']';
public final static String USAGE_REPEATING_INDICATOR = "...";
public final String getUsageSummary () {
StringBuilder usage = new StringBuilder();
boolean haveOptions = !programOptions.isEmpty();
{
String pattern = "^(.*)(\\p{Upper}.*)$";
String name = getObjectName();
Matcher matcher = Strings.getMatcher(pattern, name);
String phrase;
if (matcher.matches()) {
name = matcher.group(1);
phrase = "the " + name + ' ' + matcher.group(2);
} else {
phrase = name;
}
usage.append("Usage Summary for ").append(phrase);
}
{
usage.append("\nSyntax: ").append(getProgramName());
int start = usage.length();
if (haveOptions) {
usage.append(' ').append(USAGE_OPTIONAL_BEGIN)
.append(Option.PREFIX_CHARACTER).append("option")
.append(' ').append(USAGE_REPEATING_INDICATOR)
.append(USAGE_OPTIONAL_END);
}
if (requiredParameters != null) {
for (String parameter : requiredParameters) {
usage.append(' ').append(toOperandName(parameter));
}
}
if (optionalParameters != null) {
for (String parameter : optionalParameters) {
usage.append(' ').append(USAGE_OPTIONAL_BEGIN)
.append(toOperandName(parameter));
}
if (haveRepeatingParameter) {
usage.append(' ').append(USAGE_REPEATING_INDICATOR);
}
for (int i=optionalParameters.size(); i>0; i-=1) {
usage.append(USAGE_OPTIONAL_END);
}
}
if (usage.length() == start) usage.append(" (no arguments)");
}
{
String purpose = getPurpose();
if ((purpose != null) && !purpose.isEmpty()) {
usage.append("\n\n").append(purpose);
}
}
if (haveOptions) {
usage.append("\n\nThese options may be specified:");
for (String name : programOptions.getKeywords()) {
Option option = programOptions.get(name);
usage.append("\n ").append(Option.PREFIX_CHARACTER).append(name);
for (String operand : option.getOperands()) {
usage.append(' ').append(toOperandName(operand));
}
}
}
{
StringBuilder extension = new StringBuilder();
extendUsageSummary(extension);
String text = Strings.formatParagraphs(extension.toString());
if (!text.isEmpty()) usage.append("\n\n").append(text);
}
return usage.toString();
}
protected Program (String... arguments) {
super();
programArguments = arguments;
addOption("help",
(operands) -> {
printf("%s\n", getUsageSummary());
throw new ExitException(EXIT_CODE_SUCCESS);
}
);
}
protected void processParameters (String[] parameters)
throws SyntaxException
{
if (parameters.length > 0) {
throw new TooManyParametersException(parameters);
}
}
private final void processArguments (String[] arguments)
throws SyntaxException
{
int argumentCount = arguments.length;
int argumentIndex = 0;
while (argumentIndex < argumentCount) {
String argument = arguments[argumentIndex];
if (argument.isEmpty()) break;
if (argument.charAt(0) != Option.PREFIX_CHARACTER) break;
if (argument.length() == 1) {
argumentIndex += 1;
break;
}
String name = argument.substring(1).toLowerCase();
Option option = programOptions.get(name);
if (option == null) {
throw new SyntaxException("unknown option: %s", argument);
}
String[] operandDescriptions = option.getOperands();
int operandCount = operandDescriptions.length;
{
int index = argumentCount - argumentIndex - 1;
if (index < operandCount) {
throw new SyntaxException(
"missing %s: %c%s",
operandDescriptions[index],
Option.PREFIX_CHARACTER,
option.getName()
);
}
}
String[] operands = new String[operandCount];
System.arraycopy(arguments, argumentIndex+1, operands, 0, operandCount);
option.getHandler().handleOption(operands);
argumentIndex += 1 + operandCount;
}
int parameterCount = argumentCount - argumentIndex;
String[] parameters = new String[parameterCount];
System.arraycopy(arguments, argumentIndex, parameters, 0, parameterCount);
processParameters(parameters);
}
protected void onProgramException (ProgramException exception) {
writeProgramMessage("%s", exception.getMessage());
int exitCode;
if (exception instanceof SyntaxException) {
exitCode = EXIT_CODE_SYNTAX;
} else if (exception instanceof SemanticException) {
exitCode = EXIT_CODE_SEMANTIC;
} else if (exception instanceof ExternalException) {
exitCode = EXIT_CODE_EXTERNAL;
} else {
exitCode = EXIT_CODE_INTERNAL;
}
throw new ExitException(exitCode);
}
@Override
public final void run () {
try {
processArguments(programArguments);
runProgram();
} catch (ProgramException exception) {
onProgramException(exception);
}
}
}