Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI help to registrar #1024

Merged
merged 19 commits into from
Nov 15, 2024
Merged
36 changes: 36 additions & 0 deletions common/src/main/java/com/google/udmi/util/CommandLineOption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.google.udmi.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to indicate a command-line argument.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CommandLineOption {

String NO_VALUE = "";

/**
* Set the text help description.
*/
String description();

/**
* Short form argument.
*/
String short_form() default NO_VALUE;

/**
* Long form argument.
*/
String long_form() default NO_VALUE;

/**
* Set the argument type description.
*/
String arg_type() default NO_VALUE;
}
146 changes: 146 additions & 0 deletions common/src/main/java/com/google/udmi/util/CommandLineProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.google.udmi.util;

import static com.google.common.base.Preconditions.checkState;
import static com.google.udmi.util.GeneralUtils.ifNotNullThen;
import static com.google.udmi.util.GeneralUtils.ifNotNullThrow;
import static java.lang.String.CASE_INSENSITIVE_ORDER;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.TreeMap;

/**
* Simple command line parser class.
*/
public class CommandLineProcessor {

private static final String TERMINATING_ARG = "--";
private static final String EQUALS_SIGN = "=";
private static final int LINUX_SUCCESS_CODE = 0;
private static final int LINUX_ERROR_CODE = -1;
private final Object target;
private final Method showHelpMethod;

Map<CommandLineOption, Method> optionMap = new TreeMap<>(
(a, b) -> CASE_INSENSITIVE_ORDER.compare(getSortArg(a), getSortArg(b)));

private static String getSortArg(CommandLineOption k) {
String shortForm = k.short_form();
return shortForm.isBlank() ? k.long_form().substring(1, 2) : shortForm.substring(1, 2);
}

/**
* Create a new command line processor for the given target object.
*/
public CommandLineProcessor(Object target) {
this.target = target;

showHelpMethod = getShowHelpMethod();
optionMap.put(getShowHelpOption(), showHelpMethod);

List<Method> methods = new ArrayList<>(List.of(target.getClass().getDeclaredMethods()));
methods.forEach(method -> ifNotNullThen(method.getAnnotation(CommandLineOption.class),
a -> optionMap.put(a, method)));
optionMap.values().forEach(method -> method.setAccessible(true));
}

private Method getShowHelpMethod() {
try {
return CommandLineProcessor.class.getDeclaredMethod("showUsage");
} catch (Exception e) {
throw new RuntimeException("While getting showHelp method", e);
}
}

private CommandLineOption getShowHelpOption() {
return showHelpMethod.getAnnotation(CommandLineOption.class);
}

@CommandLineOption(short_form = "-h", description = "Show help and exit")
private void showUsage() {
showUsage(null);
}

/**
* Show program usage.
*/
public void showUsage(String message) {
ifNotNullThen(message, m -> System.err.println(m));
System.err.println("Options supported:");
optionMap.forEach((option, method) -> System.err.printf(" %s %1s %s%n",
option.short_form(), option.arg_type(), option.description()));
System.exit(message == null ? LINUX_SUCCESS_CODE : LINUX_ERROR_CODE);
}

/**
* Process the given arg list. Return a list of remaining arguments (if any).
*/
public List<String> processArgs(List<String> argList) {
try {
while (!argList.isEmpty()) {
String arg = argList.remove(0);
if (arg.equals(TERMINATING_ARG)) {
return argList;
}
Optional<Entry<CommandLineOption, Method>> first = optionMap.entrySet().stream()
.filter(option -> processArgEntry(arg, option.getKey(), option.getValue(), argList))
.findFirst();
if (first.isEmpty()) {
throw new IllegalArgumentException("Unrecognized command line option '" + arg + "'");
}
if (!arg.startsWith("-")) {
argList.add(0, arg);
return argList;
}
}
return null;
} catch (Exception e) {
showUsage(e.getMessage());
return null;
}
}

private boolean processArgEntry(String arg, CommandLineOption option, Method method,
List<String> argList) {
try {
if (arg.equals(option.short_form())) {
if (method.equals(showHelpMethod)) {
showUsage();
} else if (requiresArg(method)) {
checkState(!option.arg_type().isBlank(), "Option with argument missing type parameter");
String parameter = argList.remove(0);
method.invoke(target, parameter);
} else {
method.invoke(target);
}
return true;
} else if (!option.long_form().isBlank() && arg.startsWith(option.long_form())) {
throw new IllegalArgumentException("Long form command line not yet supported");
} else if (option.short_form().isBlank() && option.long_form().isBlank()) {
throw new IllegalArgumentException(
"Neither long nor short form not defined for " + method.getName());
} else if (!arg.startsWith("-")) {
return true;
}
return false;
} catch (Exception e) {
throw new IllegalArgumentException("Processing command line method " + method.getName(), e);
}
}

private void cleanExit(boolean success) {
System.exit(success ? LINUX_SUCCESS_CODE : LINUX_ERROR_CODE);
}

private boolean requiresArg(Method method) {
Type[] genericParameterTypes = method.getGenericParameterTypes();
checkState(genericParameterTypes.length <= 1,
"expected <= 1 parameter for command line method %s", method.getName());
return genericParameterTypes.length == 1;
}
}
9 changes: 8 additions & 1 deletion common/src/main/java/com/google/udmi/util/GeneralUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -660,11 +660,18 @@ public static String prefixedDifference(String prefix, Set<String> a, Set<String

public static String removeArg(List<String> argList, String description) {
if (argList.isEmpty()) {
throw new RuntimeException(format("Missing required %s argument", description));
throw new IllegalArgumentException(format("Missing required %s argument", description));
}
return argList.remove(0);
}

public static String removeStringArg(List<String> argList, String description) {
if (!argList.isEmpty() && argList.get(0).startsWith("-")) {
throw new IllegalArgumentException(format("Missing required %s string argument", description));
}
return removeArg(argList, description);
}

public static byte[] getFileBytes(String dataFile) {
Path dataPath = Paths.get(dataFile);
try {
Expand Down
14 changes: 10 additions & 4 deletions common/src/main/java/com/google/udmi/util/SiteModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static com.google.udmi.util.GeneralUtils.ifNotNullThen;
import static com.google.udmi.util.GeneralUtils.ifNullThen;
import static com.google.udmi.util.GeneralUtils.removeArg;
import static com.google.udmi.util.GeneralUtils.removeStringArg;
import static com.google.udmi.util.GeneralUtils.sha256;
import static com.google.udmi.util.JsonUtil.asMap;
import static com.google.udmi.util.JsonUtil.convertTo;
Expand Down Expand Up @@ -150,7 +151,7 @@ private static void loadVersionInfo(ExecutionConfiguration exeConfig) {
}

public SiteModel(String toolName, List<String> argList) {
this(removeArg(argList, "site_model"), projectSpecSupplier(argList), null);
this(removeStringArg(argList, "site_model"), projectSpecSupplier(argList), null);
ExecutionConfiguration executionConfiguration = getExecutionConfiguration();
File outFile = new File(CONFIG_OUT_DIR, format("%s_conf.json", toolName));
System.err.println("Writing reconciled configuration file to " + outFile.getAbsolutePath());
Expand All @@ -167,11 +168,16 @@ public SiteModel(ExecutionConfiguration executionConfiguration) {

private static Supplier<String> projectSpecSupplier(List<String> argList) {
return () -> {
String nextArg = argList.isEmpty() ? "" : argList.get(0);
if (nextArg.startsWith("-") && !NO_SITE.equals(nextArg)) {
if (argList.isEmpty()) {
throw new IllegalArgumentException("Missing required project spec argument");
}
String nextArg = argList.get(0);
if (nextArg.equals(NO_SITE)) {
return argList.remove(0);
} else if (nextArg.startsWith("-")) {
return null;
}
return removeArg(argList, "project_spec");
return removeStringArg(argList, "project spec");
};
}

Expand Down
2 changes: 1 addition & 1 deletion validator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
}

dependencies {
classpath 'com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:7.1.2'
classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
}
}

Expand Down
Loading
Loading