Skip to content

Commit 8144096

Browse files
authored
new: added CliArgs.parseFor (#9)
1 parent 2eaba1b commit 8144096

File tree

6 files changed

+3378
-0
lines changed

6 files changed

+3378
-0
lines changed

src/main/java/com/github/sttk/cliargs/CliArgs.java

+117
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
*/
66
package com.github.sttk.cliargs;
77

8+
import static java.util.Collections.emptyList;
9+
import static java.util.Collections.emptyMap;
10+
11+
import com.github.sttk.cliargs.annotation.Opt;
812
import com.github.sttk.exception.ReasonedException;
13+
import java.nio.file.Path;
914
import java.util.List;
1015

1116
/**
@@ -141,6 +146,40 @@ public record ConfigHasDefaultsButHasNoArg(String storeKey) {}
141146
*/
142147
public record OptionNameIsDuplicated(String storeKey, String option) {}
143148

149+
/**
150+
* Is the exception reason which indicates that default value(s) defined in
151+
* {@link Opt} annotation is failed to convert to the specified type value.
152+
*
153+
* @param storeKey A store key.
154+
* @param optArg An option argument to be converted.
155+
*/
156+
public record FailToConvertDefaultsInOptAnnotation(
157+
String storeKey, String optArg
158+
) {}
159+
160+
/**
161+
* Is the exception reason which indicates that a type of a field of the
162+
* option store is neither a boolean, a number, a string, or an array of
163+
* numbers or strings.
164+
*
165+
* @param type The field type of the option store object.
166+
* @param field The field name of the option store object.
167+
*/
168+
public record IllegalOptionType(Class<?> type, String field) {}
169+
170+
/**
171+
* Is the exception reason which indicates that it is failed to set option
172+
* arguments (including those from default values) to a field of an option
173+
* store.
174+
*
175+
* @param field The field name of the option store object.
176+
* @param type The field type of the option store object.
177+
* @param optArgs The option arguments.
178+
*/
179+
public record FailToSetOptionStoreField(
180+
String field, Class<?> type, List<?> optArgs
181+
) {}
182+
144183
///
145184

146185
/** The command name. */
@@ -234,4 +273,82 @@ public Result parse() {
234273
public Result parseWith(OptCfg[] optCfgs) {
235274
return ParseWith.parse(optCfgs, this.cmd, this.args);
236275
}
276+
277+
/**
278+
* Parses command line arguments and sets their values to the option store
279+
* which is the second argument of this method.
280+
* This method divides command line arguments to command arguments and
281+
* options, then sets the options to the option store, and returns the
282+
* command arguments with the generated option configurations.
283+
* <p>
284+
* The configurations of options are determined by types and annotations of
285+
* fields of the option store.
286+
* If the type is a boolean, the option takes no argument.
287+
* If the type is a number or a string, the option can takes single option
288+
* argument, therefore it can appear once in command line arguments.
289+
* If the type is an array, the option can takes multiple option arguments,
290+
* therefore it can appear multiple times in command line arguments.
291+
* <p>
292+
* The annotation type is @{@link Opt} and it can have annotation elements:
293+
* {@code cfg}, {@code desc}, and {@code arg}.
294+
* The {@code cfg} element can be specified names and default values(s) of
295+
* an option.
296+
* It has a special format like {@code cfg="foo-bar,f=123"}.
297+
* The first part of the element value is option names, which are separated
298+
* by commas, and ends with {@code "="} mark or end of string.
299+
* If the option name is empty or no {@code cfg} element value, the option's
300+
* name becomes same with the field name of the option store.
301+
* <p>
302+
* The string after the {@code "="} mark is default value(s).
303+
* If the type of the option is a boolean, the string after {@code "="} mark
304+
* is ignored because a boolean option takes no option argument.
305+
* If the type of the option is a number or a string, the whole string after
306+
* {@code "="} mark is default value(s).
307+
* If the type of the option is an array, the string after {@code "="} mark
308+
* have to be rounded by square brackets and separate the elements with
309+
* commas, like {@code [elem1,elem2,elem3]}.
310+
* The element separator can be used other than a comma by putting the
311+
* separator chracter before the open square bracket, like
312+
* {@code :[elem1:elem2:elem3]}.
313+
* It's useful when some array elements include commas.
314+
* <p>
315+
* <b>NOTE:</b> A default value of an empty array in {@code cfg} annotation
316+
* element is {@code []}, like {@code cfg="name=[]"}, it doesn't
317+
* represent an array which contains only one empty string.
318+
* If you want to specify an array which contains only one empty string,
319+
* write nothing after "=" mark, like {@code cfg="name="}.
320+
*
321+
* @param options An option store object.
322+
* @return A {@link Result} object that contains the parsed result.
323+
*/
324+
public Result parseFor(Object options) {
325+
OptCfg[] cfgs;
326+
try {
327+
cfgs = ParseFor.makeOptCfgsFor(options);
328+
} catch (ReasonedException e) {
329+
var cmdName = Path.of(this.cmd).getFileName().toString();
330+
var cmd = new Cmd(cmdName, emptyList(), emptyMap());
331+
return new Result(cmd, null, e);
332+
}
333+
334+
return ParseWith.parse(cfgs, this.cmd, this.args);
335+
}
336+
337+
/**
338+
* Makes an {@link OptCfg} array from the fields of the option store with the
339+
* annnotation @{@link Opt}.
340+
* <p>
341+
* About the process for {@link OptCfg} using {@link Opt} annotation, see
342+
* the comment of {@link parseFor} method.
343+
*
344+
* @param options An option store object.
345+
* @return An {@link OptCfg} array.
346+
* @throws ReasonedException If it is failed to create OptCfg objects from
347+
* the fields of the option store object.
348+
*/
349+
public static OptCfg[] makeOptCfgsFor(
350+
Object options
351+
) throws ReasonedException {
352+
return ParseFor.makeOptCfgsFor(options);
353+
}
237354
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* ParseFor class.
3+
* Copyright (C) 2024 Takayuki Sato. All Rights Reserved.
4+
*/
5+
package com.github.sttk.cliargs;
6+
7+
import static com.github.sttk.cliargs.Util.isEmpty;
8+
import static java.util.Collections.emptyList;
9+
import static java.util.Collections.emptyMap;
10+
11+
import com.github.sttk.cliargs.CliArgs.IllegalOptionType;
12+
import com.github.sttk.cliargs.CliArgs.FailToSetOptionStoreField;
13+
import com.github.sttk.cliargs.CliArgs.FailToConvertDefaultsInOptAnnotation;
14+
import com.github.sttk.cliargs.annotation.Opt;
15+
import com.github.sttk.cliargs.convert.Converter;
16+
import com.github.sttk.cliargs.OptCfg.Postparser;
17+
import com.github.sttk.exception.ReasonedException;
18+
import java.lang.reflect.Field;
19+
import java.util.List;
20+
import java.util.ArrayList;
21+
22+
interface ParseFor {
23+
24+
static OptCfg[] makeOptCfgsFor(Object options) throws ReasonedException {
25+
var list = new ArrayList<OptCfg>();
26+
27+
Class<?> cls = options.getClass();
28+
while (cls != null) {
29+
for (var fld : cls.getDeclaredFields()) {
30+
var annotation = fld.getAnnotation(Opt.class);
31+
if (annotation == null) {
32+
continue;
33+
}
34+
35+
var type = fld.getType();
36+
var hasArg = !((type == boolean.class) || (type == Boolean.class));
37+
var isArray = type.isArray();
38+
if (isArray) {
39+
type = type.getComponentType();
40+
}
41+
42+
list.add(OptCfgFactory.create(type, hasArg, isArray, fld, options));
43+
}
44+
45+
cls = cls.getSuperclass();
46+
}
47+
48+
return list.toArray(new OptCfg[list.size()]);
49+
}
50+
}
51+
52+
interface OptCfgFactory {
53+
54+
static <T> OptCfg create(
55+
Class<T> type, boolean hasArg, boolean isArray, Field fld, Object options
56+
) throws ReasonedException {
57+
var annotation = fld.getAnnotation(Opt.class);
58+
var cfg = annotation.cfg();
59+
var desc = annotation.desc();
60+
var arg = annotation.arg();
61+
62+
var storeKey = fld.getName();
63+
64+
Converter<T> converter = OptCfg.findConverter(type);
65+
if (converter == null) {
66+
if (hasArg && type != String.class) {
67+
throw new ReasonedException(new IllegalOptionType(type, storeKey));
68+
}
69+
}
70+
71+
Postparser<T> postparser = optArgs -> {
72+
try {
73+
setOptionStoreFieldValue(options, fld, optArgs, type, hasArg, isArray);
74+
} catch (Exception e) {
75+
var reason = new FailToSetOptionStoreField(storeKey, type, optArgs);
76+
throw new ReasonedException(reason, e);
77+
}
78+
};
79+
80+
var namesAndDefs = cfg.split("=", 2);
81+
var names = parseNames(namesAndDefs[0], storeKey);
82+
var defs = parseDefaults(namesAndDefs, converter, storeKey);
83+
84+
return new OptCfg(
85+
storeKey, names, hasArg, isArray, type, defs, desc, arg,
86+
converter, postparser
87+
);
88+
}
89+
90+
private static List<String> parseNames(String namesStr, String storeKey) {
91+
if (isEmpty(namesStr)) {
92+
return List.of(storeKey);
93+
}
94+
return List.of(namesStr.split(","));
95+
}
96+
97+
private static <T> List<T> parseDefaults(
98+
String[] namesAndDefaults, Converter<T> converter, String storeKey
99+
) throws ReasonedException {
100+
if (namesAndDefaults.length < 2) {
101+
return null;
102+
}
103+
104+
var str = namesAndDefaults[1];
105+
int len = str.length();
106+
107+
String[] arr = null;
108+
if (str.endsWith("]")) {
109+
if (str.startsWith("[")) {
110+
if (len > 2) {
111+
arr = str.substring(1, len-1).split(",");
112+
} else {
113+
arr = new String[0];
114+
}
115+
} else if (str.charAt(1) == '[') {
116+
if (len > 3) {
117+
var sep = str.substring(0, 1);
118+
arr = str.substring(2, len-1).split("\\" + sep); // Escape because String#split takes a regexp string.
119+
} else {
120+
arr = new String[0];
121+
}
122+
} else {
123+
arr = new String[]{str};
124+
}
125+
} else {
126+
arr = new String[]{str};
127+
}
128+
129+
var lst = new ArrayList<T>();
130+
if (converter != null) {
131+
for (var s : arr) {
132+
try {
133+
lst.add(converter.convert(s, storeKey, storeKey));
134+
} catch (Exception e) {
135+
var reason = new FailToConvertDefaultsInOptAnnotation(storeKey, str);
136+
throw new ReasonedException(reason, e);
137+
}
138+
}
139+
} else {
140+
for (var s : arr) {
141+
@SuppressWarnings("unchecked")
142+
T t = (T) s;
143+
lst.add(t);
144+
}
145+
}
146+
147+
return lst;
148+
}
149+
150+
private static <T> void setOptionStoreFieldValue(
151+
Object options, Field fld, List<T> optArgs, Class<T> type,
152+
boolean hasArg, boolean isArray
153+
) throws Exception {
154+
if (optArgs == null) {
155+
return;
156+
}
157+
158+
if (type == boolean.class || type == Boolean.class) {
159+
fld.setAccessible(true);
160+
fld.set(options, true);
161+
return;
162+
}
163+
164+
if (! isArray) {
165+
fld.setAccessible(true);
166+
fld.set(options, optArgs.get(0));
167+
return;
168+
}
169+
170+
int n = optArgs.size();
171+
if (type == int.class) {
172+
int[] arr = new int[n];
173+
for (int i = 0; i < n; i++) {
174+
arr[i] = Integer.class.cast(optArgs.get(i));
175+
}
176+
fld.setAccessible(true);
177+
fld.set(options, arr);
178+
} else if (type == double.class) {
179+
double[] arr = new double[n];
180+
for (int i = 0; i < n; i++) {
181+
arr[i] = Double.class.cast(optArgs.get(i));
182+
}
183+
fld.setAccessible(true);
184+
fld.set(options, arr);
185+
} else if (type == long.class) {
186+
long[] arr = new long[n];
187+
for (int i = 0; i < n; i++) {
188+
arr[i] = Long.class.cast(optArgs.get(i));
189+
}
190+
fld.setAccessible(true);
191+
fld.set(options, arr);
192+
} else if (type == float.class) {
193+
float[] arr = new float[n];
194+
for (int i = 0; i < n; i++) {
195+
arr[i] = Float.class.cast(optArgs.get(i));
196+
}
197+
fld.setAccessible(true);
198+
fld.set(options, arr);
199+
} else if (type == short.class) {
200+
short[] arr = new short[n];
201+
for (int i = 0; i < n; i++) {
202+
arr[i] = Short.class.cast(optArgs.get(i));
203+
}
204+
fld.setAccessible(true);
205+
fld.set(options, arr);
206+
} else if (type == byte.class) {
207+
byte[] arr = new byte[n];
208+
for (int i = 0; i < n; i++) {
209+
arr[i] = Byte.class.cast(optArgs.get(i));
210+
}
211+
fld.setAccessible(true);
212+
fld.set(options, arr);
213+
} else {
214+
@SuppressWarnings("unchecked")
215+
T[] arr = (T[]) java.lang.reflect.Array.newInstance(type, n);
216+
for (int i = 0; i < n; i++) {
217+
arr[i] = optArgs.get(i);
218+
}
219+
fld.setAccessible(true);
220+
fld.set(options, arr);
221+
}
222+
}
223+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (C) 2024 Takayuki Sato. All Rights Reserved.
3+
* This program is free software under MIT License.
4+
* See the file LICENSE in this distribution for more details.
5+
*/
6+
package com.github.sttk.cliargs.annotation;
7+
8+
import com.github.sttk.cliargs.OptCfg;
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
/**
15+
* Is the annotation that is attached to fields of an option store class.
16+
* <p>
17+
* This annotation can specify values for the fields of an {@link OptCfg}
18+
* object: {@code names}, {@code defaults}, {@code desc} and {@code argInHelp}.
19+
*/
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@Target(ElementType.FIELD)
22+
public @interface Opt {
23+
24+
/** Gets the option name and aliases. */
25+
String cfg() default "";
26+
27+
/** Gets the description of the option. */
28+
String desc() default "";
29+
30+
/** Gets the display of the option argument in a help text. */
31+
String arg() default "";
32+
}

0 commit comments

Comments
 (0)