Skip to content

Commit a4464dd

Browse files
authored
Add CLI help to registrar (#1024)
1 parent 8c66bd7 commit a4464dd

File tree

6 files changed

+247
-61
lines changed

6 files changed

+247
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.google.udmi.util;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Annotation to indicate a command-line argument.
10+
*/
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@Target({ElementType.METHOD})
13+
public @interface CommandLineOption {
14+
15+
String NO_VALUE = "";
16+
17+
/**
18+
* Set the text help description.
19+
*/
20+
String description();
21+
22+
/**
23+
* Short form argument.
24+
*/
25+
String short_form() default NO_VALUE;
26+
27+
/**
28+
* Long form argument.
29+
*/
30+
String long_form() default NO_VALUE;
31+
32+
/**
33+
* Set the argument type description.
34+
*/
35+
String arg_type() default NO_VALUE;
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.google.udmi.util;
2+
3+
import static com.google.common.base.Preconditions.checkState;
4+
import static com.google.udmi.util.GeneralUtils.ifNotNullThen;
5+
import static com.google.udmi.util.GeneralUtils.ifNotNullThrow;
6+
import static java.lang.String.CASE_INSENSITIVE_ORDER;
7+
8+
import java.lang.reflect.Method;
9+
import java.lang.reflect.Type;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Map.Entry;
14+
import java.util.Optional;
15+
import java.util.TreeMap;
16+
17+
/**
18+
* Simple command line parser class.
19+
*/
20+
public class CommandLineProcessor {
21+
22+
private static final String TERMINATING_ARG = "--";
23+
private static final String EQUALS_SIGN = "=";
24+
private static final int LINUX_SUCCESS_CODE = 0;
25+
private static final int LINUX_ERROR_CODE = -1;
26+
private final Object target;
27+
private final Method showHelpMethod;
28+
29+
Map<CommandLineOption, Method> optionMap = new TreeMap<>(
30+
(a, b) -> CASE_INSENSITIVE_ORDER.compare(getSortArg(a), getSortArg(b)));
31+
32+
private static String getSortArg(CommandLineOption k) {
33+
String shortForm = k.short_form();
34+
return shortForm.isBlank() ? k.long_form().substring(1, 2) : shortForm.substring(1, 2);
35+
}
36+
37+
/**
38+
* Create a new command line processor for the given target object.
39+
*/
40+
public CommandLineProcessor(Object target) {
41+
this.target = target;
42+
43+
showHelpMethod = getShowHelpMethod();
44+
optionMap.put(getShowHelpOption(), showHelpMethod);
45+
46+
List<Method> methods = new ArrayList<>(List.of(target.getClass().getDeclaredMethods()));
47+
methods.forEach(method -> ifNotNullThen(method.getAnnotation(CommandLineOption.class),
48+
a -> optionMap.put(a, method)));
49+
optionMap.values().forEach(method -> method.setAccessible(true));
50+
}
51+
52+
private Method getShowHelpMethod() {
53+
try {
54+
return CommandLineProcessor.class.getDeclaredMethod("showUsage");
55+
} catch (Exception e) {
56+
throw new RuntimeException("While getting showHelp method", e);
57+
}
58+
}
59+
60+
private CommandLineOption getShowHelpOption() {
61+
return showHelpMethod.getAnnotation(CommandLineOption.class);
62+
}
63+
64+
@CommandLineOption(short_form = "-h", description = "Show help and exit")
65+
private void showUsage() {
66+
showUsage(null);
67+
}
68+
69+
/**
70+
* Show program usage.
71+
*/
72+
public void showUsage(String message) {
73+
ifNotNullThen(message, m -> System.err.println(m));
74+
System.err.println("Options supported:");
75+
optionMap.forEach((option, method) -> System.err.printf(" %s %1s %s%n",
76+
option.short_form(), option.arg_type(), option.description()));
77+
System.exit(message == null ? LINUX_SUCCESS_CODE : LINUX_ERROR_CODE);
78+
}
79+
80+
/**
81+
* Process the given arg list. Return a list of remaining arguments (if any).
82+
*/
83+
public List<String> processArgs(List<String> argList) {
84+
try {
85+
while (!argList.isEmpty()) {
86+
String arg = argList.remove(0);
87+
if (arg.equals(TERMINATING_ARG)) {
88+
return argList;
89+
}
90+
Optional<Entry<CommandLineOption, Method>> first = optionMap.entrySet().stream()
91+
.filter(option -> processArgEntry(arg, option.getKey(), option.getValue(), argList))
92+
.findFirst();
93+
if (first.isEmpty()) {
94+
throw new IllegalArgumentException("Unrecognized command line option '" + arg + "'");
95+
}
96+
if (!arg.startsWith("-")) {
97+
argList.add(0, arg);
98+
return argList;
99+
}
100+
}
101+
return null;
102+
} catch (Exception e) {
103+
showUsage(e.getMessage());
104+
return null;
105+
}
106+
}
107+
108+
private boolean processArgEntry(String arg, CommandLineOption option, Method method,
109+
List<String> argList) {
110+
try {
111+
if (arg.equals(option.short_form())) {
112+
if (method.equals(showHelpMethod)) {
113+
showUsage();
114+
} else if (requiresArg(method)) {
115+
checkState(!option.arg_type().isBlank(), "Option with argument missing type parameter");
116+
String parameter = argList.remove(0);
117+
method.invoke(target, parameter);
118+
} else {
119+
method.invoke(target);
120+
}
121+
return true;
122+
} else if (!option.long_form().isBlank() && arg.startsWith(option.long_form())) {
123+
throw new IllegalArgumentException("Long form command line not yet supported");
124+
} else if (option.short_form().isBlank() && option.long_form().isBlank()) {
125+
throw new IllegalArgumentException(
126+
"Neither long nor short form not defined for " + method.getName());
127+
} else if (!arg.startsWith("-")) {
128+
return true;
129+
}
130+
return false;
131+
} catch (Exception e) {
132+
throw new IllegalArgumentException("Processing command line method " + method.getName(), e);
133+
}
134+
}
135+
136+
private void cleanExit(boolean success) {
137+
System.exit(success ? LINUX_SUCCESS_CODE : LINUX_ERROR_CODE);
138+
}
139+
140+
private boolean requiresArg(Method method) {
141+
Type[] genericParameterTypes = method.getGenericParameterTypes();
142+
checkState(genericParameterTypes.length <= 1,
143+
"expected <= 1 parameter for command line method %s", method.getName());
144+
return genericParameterTypes.length == 1;
145+
}
146+
}

common/src/main/java/com/google/udmi/util/GeneralUtils.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,18 @@ public static String prefixedDifference(String prefix, Set<String> a, Set<String
660660

661661
public static String removeArg(List<String> argList, String description) {
662662
if (argList.isEmpty()) {
663-
throw new RuntimeException(format("Missing required %s argument", description));
663+
throw new IllegalArgumentException(format("Missing required %s argument", description));
664664
}
665665
return argList.remove(0);
666666
}
667667

668+
public static String removeStringArg(List<String> argList, String description) {
669+
if (!argList.isEmpty() && argList.get(0).startsWith("-")) {
670+
throw new IllegalArgumentException(format("Missing required %s string argument", description));
671+
}
672+
return removeArg(argList, description);
673+
}
674+
668675
public static byte[] getFileBytes(String dataFile) {
669676
Path dataPath = Paths.get(dataFile);
670677
try {

common/src/main/java/com/google/udmi/util/SiteModel.java

+10-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static com.google.udmi.util.GeneralUtils.ifNotNullThen;
1919
import static com.google.udmi.util.GeneralUtils.ifNullThen;
2020
import static com.google.udmi.util.GeneralUtils.removeArg;
21+
import static com.google.udmi.util.GeneralUtils.removeStringArg;
2122
import static com.google.udmi.util.GeneralUtils.sha256;
2223
import static com.google.udmi.util.JsonUtil.asMap;
2324
import static com.google.udmi.util.JsonUtil.convertTo;
@@ -150,7 +151,7 @@ private static void loadVersionInfo(ExecutionConfiguration exeConfig) {
150151
}
151152

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

168169
private static Supplier<String> projectSpecSupplier(List<String> argList) {
169170
return () -> {
170-
String nextArg = argList.isEmpty() ? "" : argList.get(0);
171-
if (nextArg.startsWith("-") && !NO_SITE.equals(nextArg)) {
171+
if (argList.isEmpty()) {
172+
throw new IllegalArgumentException("Missing required project spec argument");
173+
}
174+
String nextArg = argList.get(0);
175+
if (nextArg.equals(NO_SITE)) {
176+
return argList.remove(0);
177+
} else if (nextArg.startsWith("-")) {
172178
return null;
173179
}
174-
return removeArg(argList, "project_spec");
180+
return removeStringArg(argList, "project spec");
175181
};
176182
}
177183

validator/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ buildscript {
66
}
77

88
dependencies {
9-
classpath 'com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:7.1.2'
9+
classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
1010
}
1111
}
1212

0 commit comments

Comments
 (0)