Skip to content

Commit 88a6a39

Browse files
committed
Up our CLI game
1 parent 916e066 commit 88a6a39

20 files changed

Lines changed: 1035 additions & 400 deletions

src/main/java/net/royalur/LutVisualisation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package net.royalur;
22

3+
import net.royalur.cli.CLI;
34
import net.royalur.lut.GameStateEncoding;
45
import net.royalur.lut.Lut;
5-
import net.royalur.lut.LutCLI;
66
import net.royalur.lut.buffer.UInt8ValueBuffer;
77
import net.royalur.lut.store.DataSink;
88
import net.royalur.lut.store.LutMap;
@@ -88,7 +88,7 @@ public void calculateDepths(File outputFile) throws IOException {
8888
break;
8989

9090
double durationMS = (System.nanoTime() - start) / 1e6d;
91-
System.out.println(".. " + (++index) + " (" + LutCLI.MS_DURATION.format(durationMS) + ")");
91+
System.out.println(".. " + (++index) + " (" + CLI.MS_DURATION.format(durationMS) + ")");
9292
}
9393
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
9494
ByteBuffer workingBuffer = ByteBuffer.allocateDirect(1024 * 1024);
Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,49 @@
11
package net.royalur;
22

33
import net.royalur.cli.*;
4-
import net.royalur.lut.LutCLI;
4+
import net.royalur.lut.cli.LutCommand;
5+
import net.royalur.stats.cli.StatsCommand;
56

6-
import javax.annotation.Nullable;
7-
import java.io.IOException;
87
import java.io.PrintStream;
98
import java.util.logging.Logger;
109

11-
public class Main {
10+
public class Main extends CLIRoutingCommand {
1211

1312
private static final Logger LOG = Logger.getLogger(Main.class.getName());
1413

15-
private Main() {}
16-
17-
public static void printHelp(PrintStream out) {
18-
out.println("RoyalUr-Java CLI Usage:");
19-
out.println("* lut - Commands for generating and managing solved game lookup tables (luts)");
20-
out.println(" lut train: Generate a new solved game lookup-table, or refine an existing one");
21-
out.println(" lut read [file]: Read metadata about an existing solved game lookup-table");
22-
out.println("* stats - Commands to calculate game statistics");
23-
out.println(" stats count [rulesets]: Count the number of states in rule sets");
14+
private Main() {
15+
super(null, "", "RoyalUr-Java CLI");
16+
addSubCommand(new LutCommand(this));
17+
addSubCommand(new StatsCommand(this));
2418
}
2519

26-
private static @Nullable CLIHandler routeCLIRequest(CLI cli) throws IOException {
27-
cli.setHelp(Main::printHelp);
28-
if (!cli.hasNext())
29-
return null;
30-
31-
String command = cli.next();
32-
CLIHandler handler;
33-
34-
if (command.equalsIgnoreCase("lut"))
35-
return LutCLI.routeCLIRequest(cli);
36-
if (command.equalsIgnoreCase("stats"))
37-
return StatsCLI.routeCLIRequest(cli);
38-
39-
throw new CLIBadCommandException("Unknown command: " + command);
20+
private void printHelp(CLI cli, PrintStream out) {
21+
cli.printHelp(out);
22+
out.println("Global Options:");
23+
out.println(" --help Show this help message");
24+
out.println();
4025
}
4126

42-
public static void main(String[] args) throws IOException {
27+
public void handle(String[] args) throws Exception {
4328
CLI cli = null;
4429
try {
4530
cli = CLI.parse(args);
46-
boolean showHelp = cli.readKeywordIsPresent("help");
31+
boolean showHelp = cli.readKeywordIsPresent("--help");
4732

4833
CLIHandler handler = null;
4934
if (showHelp) {
5035
// Consume any CLI errors, and just print the help that the routing sets.
5136
try {
52-
handler = routeCLIRequest(cli);
37+
handler = handle(cli);
5338
} catch (CLIException e) { /* Ignored */ }
5439
} else {
55-
handler = routeCLIRequest(cli);
40+
handler = handle(cli);
5641
}
5742

5843
// If --help is present, print the help.
5944
// If the routing returns null, we also print help.
6045
if (showHelp || handler == null) {
61-
cli.printHelp(System.out);
46+
printHelp(cli, System.out);
6247
return;
6348
}
6449

@@ -72,9 +57,15 @@ public static void main(String[] args) throws IOException {
7257
} catch (CLIBadCommandException e) {
7358
System.err.println(e.getMessage());
7459
if (cli != null) {
75-
cli.printHelp(System.err);
60+
System.err.println();
61+
printHelp(cli, System.err);
7662
}
7763
System.exit(1);
7864
}
7965
}
66+
67+
public static void main(String[] args) throws Exception {
68+
Main main = new Main();
69+
main.handle(args);
70+
}
8071
}

src/main/java/net/royalur/cli/CLI.java

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,36 @@
44
import javax.annotation.Nullable;
55
import java.io.File;
66
import java.io.PrintStream;
7+
import java.text.DecimalFormat;
78
import java.util.*;
8-
import java.util.function.Consumer;
99

1010
/**
1111
* Parses CLI arguments.
1212
*/
1313
public class CLI {
1414

15+
public static final DecimalFormat MS_DURATION = new DecimalFormat("#,###");
16+
public static final int MIN_COMMAND_WIDTH = 24;
17+
public static final int MIN_ARGUMENT_WIDTH = 34;
18+
1519
private final String[] positionalArgs;
1620
private int positionalArgsIndex = 0;
1721

1822
private final Map<String, String> remainingKeywordArgs;
19-
private @Nullable Consumer<PrintStream> help;
23+
private @Nullable CLICommand currentCommand;
2024

2125
public CLI(String[] positionalArgs, Map<String, String> keywordArgs) {
2226
this.positionalArgs = positionalArgs;
2327
this.remainingKeywordArgs = keywordArgs;
2428
}
2529

30+
/**
31+
* Used for things like printing help, or for help in error reporting.
32+
*/
33+
public void setCurrentCommand(@Nonnull CLICommand currentCommand) {
34+
this.currentCommand = currentCommand;
35+
}
36+
2637
public static CLI parse(String[] args) {
2738
List<String> positionalArgs = new ArrayList<>();
2839
Map<String, String> keywordArgs = new HashMap<>();
@@ -34,11 +45,11 @@ public static CLI parse(String[] args) {
3445
int eqIndex = arg.indexOf("=");
3546
if (eqIndex >= 0) {
3647
// Handle --keyword=value.
37-
key = arg.substring(2, eqIndex);
48+
key = arg.substring(0, eqIndex);
3849
value = arg.substring(eqIndex + 1);
3950
} else {
4051
// Handle --keyword.
41-
key = arg.substring(2);
52+
key = arg;
4253
value = "";
4354
}
4455
keywordArgs.put(key, value);
@@ -49,17 +60,9 @@ public static CLI parse(String[] args) {
4960
return new CLI(positionalArgs.toArray(new String[0]), keywordArgs);
5061
}
5162

52-
/**
53-
* As sub-commands are routed, they can update the help that is displayed
54-
* when an error occurs.
55-
*/
56-
public void setHelp(@Nonnull Consumer<PrintStream> help) {
57-
this.help = help;
58-
}
59-
6063
public void printHelp(PrintStream out) {
61-
if (help != null) {
62-
help.accept(out);
64+
if (currentCommand != null) {
65+
currentCommand.printHelp(out);
6366
}
6467
}
6568

@@ -89,7 +92,7 @@ public File nextExistingDirectory() {
8992
private void assertNotEmpty(String keyword, String value) {
9093
if (value.isEmpty()) {
9194
throw new CLIArgumentException(
92-
"Value of --" + keyword + " is empty, expected --" + keyword + "=value"
95+
"Value of " + keyword + " is empty, expected " + keyword + "=value"
9396
);
9497
}
9598
}
@@ -190,4 +193,24 @@ private static File parseExistingDirectory(String filename) {
190193

191194
return file;
192195
}
196+
197+
public static String rightPad(String input, int minLength) {
198+
if (input.length() >= minLength)
199+
return input;
200+
201+
StringBuilder builder = new StringBuilder(minLength);
202+
builder.append(input);
203+
while (builder.length() < minLength) {
204+
builder.append(" ");
205+
}
206+
return builder.toString();
207+
}
208+
209+
public static String rightPadCommand(String command) {
210+
return rightPad(command, MIN_COMMAND_WIDTH);
211+
}
212+
213+
public static String rightPadArgument(String argument) {
214+
return rightPad(argument, MIN_ARGUMENT_WIDTH);
215+
}
193216
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package net.royalur.cli;
2+
3+
public record CLIArgument(
4+
String name,
5+
CLIArgumentType type,
6+
String[] descriptionLines,
7+
boolean required
8+
) {
9+
10+
public CLIArgument {
11+
for (String line : descriptionLines) {
12+
if (line.length() > 60)
13+
throw new IllegalArgumentException("Description line too long: " + line.length() + " > 60");
14+
}
15+
if (!name.startsWith("-")) {
16+
if (required && (!name.startsWith("<") || !name.endsWith(">")))
17+
throw new IllegalArgumentException("Required args should be surrounded by angled brackets <>");
18+
if (!required && (!name.startsWith("[") || !name.endsWith("]")))
19+
throw new IllegalArgumentException("Optional args should be surrounded by square brackets []");
20+
}
21+
}
22+
23+
public boolean isKeywordArg() {
24+
return name.startsWith("-");
25+
}
26+
27+
public boolean isPositionalArg() {
28+
return !isKeywordArg();
29+
}
30+
31+
private void appendNameAndType(StringBuilder builder) {
32+
builder.append(name);
33+
if (isKeywordArg() && !type.equals(CLIArgumentType.NONE)) {
34+
builder.append("=<").append(type.name().toLowerCase()).append(">");
35+
}
36+
}
37+
38+
public String getNameAndType() {
39+
StringBuilder builder = new StringBuilder();
40+
appendNameAndType(builder);
41+
return builder.toString();
42+
}
43+
44+
@Override
45+
public String toString() {
46+
StringBuilder builder = new StringBuilder();
47+
appendNameAndType(builder);
48+
if (isKeywordArg() && required) {
49+
builder.append(" (required)");
50+
}
51+
return builder.toString();
52+
}
53+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package net.royalur.cli;
2+
3+
import javax.annotation.Nullable;
4+
5+
public record CLIArgumentType(String name) {
6+
public static final CLIArgumentType NONE = new CLIArgumentType("None");
7+
public static final CLIArgumentType UNKNOWN = new CLIArgumentType("Unknown");
8+
public static final CLIArgumentType INTEGER = new CLIArgumentType("Integer");
9+
public static final CLIArgumentType NUMBER = new CLIArgumentType("Number");
10+
public static final CLIArgumentType FILE = new CLIArgumentType("File");
11+
public static final CLIArgumentType TEXT = new CLIArgumentType("Text");
12+
public static final CLIArgumentType SUBCOMMAND = new CLIArgumentType("Sub-Command");
13+
public static final CLIArgumentType GAME_SETTINGS = new CLIArgumentType("Game Settings");
14+
public static final CLIArgumentType VALUE_TYPE = new CLIArgumentType("Value Type");
15+
16+
@Override
17+
public boolean equals(@Nullable Object obj) {
18+
if (this == obj)
19+
return true;
20+
if (!getClass().equals(obj.getClass()))
21+
return false;
22+
23+
CLIArgumentType other = (CLIArgumentType) obj;
24+
return name.equals(other.name);
25+
}
26+
}

0 commit comments

Comments
 (0)