Skip to content

Commit 148e3ce

Browse files
feat: Do most (not all yet) of the core CLI parsing stuff [WIP]
1 parent b6df9f6 commit 148e3ce

7 files changed

Lines changed: 160 additions & 7 deletions

File tree

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package net.marcellperger.mathexpr.cli.minicli;
22

3+
import org.jetbrains.annotations.Nullable;
4+
35
import java.util.List;
46

57
public class BooleanCLIOption extends CLIOption<Boolean> {
@@ -8,11 +10,22 @@ public BooleanCLIOption(List<String> optionNames) {
810
}
911

1012
@Override
11-
public void setValueFromString(String s) {
12-
setValue(switch (s.strip().toLowerCase()) {
13+
protected void _setValueFromString(@Nullable String s) {
14+
if(s == null) setValue(true);
15+
else setValue(switch (s.strip().toLowerCase()) {
1316
case "0", "no", "false" -> false;
14-
case "1", "yes", "true" -> true;
15-
case String s2 -> throw new IllegalArgumentException("Bad boolean value '" + s2 + "'"); // TODO special CLIParseException
17+
case "", "1", "yes", "true" -> true;
18+
case String s2 -> throw new CLIParseException("Bad boolean value '" + s2 + "'");
1619
});
1720
}
21+
22+
@Override
23+
public boolean supportsSeparateValueAfterShortForm() {
24+
return false; // cannot have `foo -r no` (use `-r=no` / `--long-form=no`
25+
}
26+
27+
@Override
28+
public OptionValueMode getValueMode() {
29+
return OptionValueMode.OPTIONAL;
30+
}
1831
}

src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIOption.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package net.marcellperger.mathexpr.cli.minicli;
22

3+
import org.jetbrains.annotations.NotNull;
34
import org.jetbrains.annotations.Nullable;
45

56
import java.util.List;
7+
import java.util.Objects;
68

79
public abstract class CLIOption<T> {
810
List<String> names;
@@ -25,7 +27,23 @@ public void setValue(@Nullable T value) {
2527
this.value = value;
2628
}
2729

28-
public abstract void setValueFromString(String s);
30+
// Nullable because we need a way to distinguish `--foo=''` and `--foo`
31+
protected abstract void _setValueFromString(@Nullable String s);
32+
public void setValueFromString(@Nullable String s) {
33+
getValueMode().validateHasValue(s == null);
34+
_setValueFromString(s);
35+
}
36+
37+
public OptionValueMode getValueMode() {
38+
return OptionValueMode.REQUIRED;
39+
}
40+
41+
/**
42+
* @return True if it supports `-b arg`, False to make `-b arg` into `-b`,`arg`
43+
*/
44+
public boolean supportsSeparateValueAfterShortForm() {
45+
return getValueMode() != OptionValueMode.NONE;
46+
}
2947

3048
public void reset() {
3149
value = null;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.marcellperger.mathexpr.cli.minicli;
2+
3+
public class CLIParseException extends IllegalArgumentException {
4+
public CLIParseException() {
5+
}
6+
7+
public CLIParseException(String s) {
8+
super(s);
9+
}
10+
11+
public CLIParseException(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
15+
public CLIParseException(Throwable cause) {
16+
super(cause);
17+
}
18+
}

src/main/java/net/marcellperger/mathexpr/cli/minicli/MiniCLI.java

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package net.marcellperger.mathexpr.cli.minicli;
22

3+
import net.marcellperger.mathexpr.util.Pair;
4+
import net.marcellperger.mathexpr.util.Util;
35
import org.jetbrains.annotations.Contract;
46
import org.jetbrains.annotations.NotNull;
57
import org.jetbrains.annotations.Nullable;
@@ -8,6 +10,7 @@
810
import java.util.HashMap;
911
import java.util.List;
1012
import java.util.Map;
13+
import java.util.Objects;
1114

1215
public class MiniCLI {
1316
Map<String, CLIOption<?>> options = new HashMap<>();
@@ -40,8 +43,73 @@ public MiniCLI parseArgs(String[] args) {
4043
}
4144
@Contract("_ -> this")
4245
public MiniCLI parseArgs(List<String> args) {
43-
46+
new Parser(args).parse();
4447
return this;
48+
}
49+
private @NotNull CLIOption<?> lookupOption(String opt) {
50+
CLIOption<?> out = options.get(opt);
51+
if(out == null) throw new CLIParseException("'%s' is not a valid option".formatted(opt));
52+
return out;
53+
}
54+
private class Parser {
55+
@Nullable CLIOption<?> prev = null;
56+
List<String> args;
57+
58+
public Parser(List<String> args) {
59+
this.args = args;
60+
}
61+
62+
public void parse() {
63+
args.forEach(this::pumpSingleArg);
64+
// TODO: finalize: check prev, handle if not null
65+
}
66+
67+
// Make this public as it could be useful for making stream-ing type stuff
68+
public void pumpSingleArg(String arg) {
69+
ArgType argT = ArgType.fromArg(arg);
70+
if(prev != null && prev.getValueMode() == OptionValueMode.REQUIRED) {
71+
// We NEED a value so force it, even if it looks like a flag
72+
flushPrevWithValue(arg);
73+
return;
74+
}
75+
switch (argT) {
76+
case NORMAL -> {
77+
if (prev == null) positionalArgs.add(arg);
78+
else flushPrevWithValue(arg);
79+
}
80+
case SINGLE -> {
81+
if(prev != null) flushPrev();
82+
if(arg.contains("=")) {
83+
Pair<String, String> kv = Pair.ofArray(arg.split("=", 2));
84+
CLIOption<?> opt = lookupOption(kv.left);
85+
opt.setValueFromString(kv.right);
86+
} else {
87+
prev = lookupOption(arg);
88+
// flush immediately if next one cannot be a value
89+
if(!prev.supportsSeparateValueAfterShortForm()) flushPrev();
90+
}
91+
}
92+
case DOUBLE -> {
93+
if(prev != null) flushPrev();
94+
95+
// TODO lookup here
96+
prev = null;
97+
}
98+
}
99+
}
100+
101+
private void flushPrev() {
102+
Util.realAssert(prev != null, "flushPrev must have a `prev` to flush");
103+
prev = null;
104+
}
105+
private void flushPrevWithValue(String value) {
106+
Util.realAssert(prev != null, "flushPrevWithValue must have a `prev` to flush");
107+
prev.setValueFromString(value);
108+
prev = null;
109+
}
110+
}
111+
private record PrevState(CLIOption<?> option, @Nullable String value) {
112+
45113
}
46114
private enum ArgType {
47115
NORMAL, SINGLE, DOUBLE;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.marcellperger.mathexpr.cli.minicli;
2+
3+
/**
4+
* Controls if a value is required for an option
5+
*/
6+
public enum OptionValueMode {
7+
/**
8+
* Specifying a value is not allowed (e.g. {@code --foo} is allowed, {@code --foo=76} is not)
9+
*/
10+
NONE,
11+
/**
12+
* Specifying a value is optional (e.g. {@code --foo} and {@code --foo=76} are both allowed)
13+
*/
14+
OPTIONAL,
15+
/**
16+
* Specifying a value is required (e.g. {@code --foo=76} is allowed, {@code --foo} is not)
17+
*/
18+
REQUIRED,
19+
;
20+
public void validateHasValue(boolean hasValue) {
21+
switch (this) {
22+
case NONE -> {
23+
if(hasValue) throw new CLIParseException("Specifying a value for this option is not allowed");
24+
}
25+
case OPTIONAL -> {}
26+
case REQUIRED -> {
27+
if(!hasValue) throw new CLIParseException("This option requires a value to be passed");
28+
}
29+
}
30+
}
31+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package net.marcellperger.mathexpr.cli.minicli;
22

3+
import org.jetbrains.annotations.Nullable;
4+
35
import java.util.List;
46

57
public class StringCLIOption extends CLIOption<String> {
@@ -8,7 +10,7 @@ public StringCLIOption(List<String> keys) {
810
}
911

1012
@Override
11-
public void setValueFromString(String s) {
13+
protected void _setValueFromString(@Nullable String s) {
1214
setValue(s);
1315
}
1416
}

0 commit comments

Comments
 (0)