From 3c70b3a3bd70c34867281ce4f019c60f55c63f75 Mon Sep 17 00:00:00 2001 From: hneemann Date: Mon, 8 Jun 2020 16:23:57 +0200 Subject: [PATCH] first working cli interface implementation --- src/main/java/CLI.java | 32 ++++ .../java/de/neemann/digital/cli/Argument.java | 103 +++++++++++++ .../de/neemann/digital/cli/CLICommand.java | 31 ++++ .../de/neemann/digital/cli/CLIException.java | 60 ++++++++ .../de/neemann/digital/cli/CLITester.java | 43 ++++++ .../java/de/neemann/digital/cli/Muxer.java | 84 +++++++++++ .../de/neemann/digital/cli/SimpleCommand.java | 142 ++++++++++++++++++ .../de/neemann/digital/cli/package-info.java | 10 ++ src/main/java/package-info.java | 9 ++ src/main/resources/lang/lang_de.xml | 18 +++ src/main/resources/lang/lang_en.xml | 19 +++ .../de/neemann/digital/cli/ArgumentTest.java | 55 +++++++ .../digital/cli/SimpleCommandTest.java | 103 +++++++++++++ .../de/neemann/digital/lang/TestLang.java | 2 +- 14 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 src/main/java/CLI.java create mode 100644 src/main/java/de/neemann/digital/cli/Argument.java create mode 100644 src/main/java/de/neemann/digital/cli/CLICommand.java create mode 100644 src/main/java/de/neemann/digital/cli/CLIException.java create mode 100644 src/main/java/de/neemann/digital/cli/CLITester.java create mode 100644 src/main/java/de/neemann/digital/cli/Muxer.java create mode 100644 src/main/java/de/neemann/digital/cli/SimpleCommand.java create mode 100644 src/main/java/de/neemann/digital/cli/package-info.java create mode 100644 src/main/java/package-info.java create mode 100644 src/test/java/de/neemann/digital/cli/ArgumentTest.java create mode 100644 src/test/java/de/neemann/digital/cli/SimpleCommandTest.java diff --git a/src/main/java/CLI.java b/src/main/java/CLI.java new file mode 100644 index 000000000..04d0382cb --- /dev/null +++ b/src/main/java/CLI.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ + +import de.neemann.digital.cli.CLIException; +import de.neemann.digital.cli.Muxer; + +/** + * Entry point for the CLI interface + */ +public final class CLI { + + private CLI() { + } + + /** + * Entry point for the CLI interface + * + * @param args the command line arguments + */ + public static void main(String[] args) { + try { + Muxer.MAIN_MUXER.execute(args); + } catch (CLIException e) { + e.printMessage(System.out); + Muxer.MAIN_MUXER.printDescription(System.out, ""); + System.exit(e.getExitCode()); + } + } +} diff --git a/src/main/java/de/neemann/digital/cli/Argument.java b/src/main/java/de/neemann/digital/cli/Argument.java new file mode 100644 index 000000000..4f7b0730b --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/Argument.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import de.neemann.digital.lang.Lang; + +/** + * A command cline argument + * + * @param the type of the argument + */ +public class Argument { + private final String name; + private final boolean optional; + private T value; + private boolean isSet; + + /** + * Creates a new argument + * + * @param name the name of the argument + * @param def the default value + * @param optional true if argument is optional + */ + public Argument(String name, T def, boolean optional) { + this.name = name; + this.optional = optional; + if (def == null) + throw new NullPointerException(); + value = def; + } + + T get() { + return value; + } + + @Override + public String toString() { + if (optional) + return "[[" + name + "]]"; + else + return "[" + name + "]"; + } + + /** + * Sets a string value + * + * @param val the value to set + * @throws CLIException CLIException + */ + public void setString(String val) throws CLIException { + if (value instanceof String) + value = (T) val; + else if (value instanceof Boolean) + switch (val.toLowerCase()) { + case "yes": + case "1": + case "true": + value = (T) (Boolean) true; + break; + case "no": + case "0": + case "false": + value = (T) (Boolean) false; + break; + default: + throw new CLIException(Lang.get("cli_notABool_N", val), 106); + } + else if (value instanceof Integer) { + try { + value = (T) (Integer) Integer.parseInt(val); + } catch (NumberFormatException e) { + throw new CLIException(Lang.get("cli_notANumber_N", val), e); + } + } else + throw new CLIException(Lang.get("cli_invalidType_N", value.getClass().getSimpleName()), 203); + isSet = true; + } + + /** + * @return if this argument was set + */ + public boolean isSet() { + return isSet; + } + + /** + * @return the name of this argument + */ + public String getName() { + return name; + } + + /** + * @return true if this argument is optional + */ + public boolean isOptional() { + return optional; + } +} diff --git a/src/main/java/de/neemann/digital/cli/CLICommand.java b/src/main/java/de/neemann/digital/cli/CLICommand.java new file mode 100644 index 000000000..ababf9e51 --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/CLICommand.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import java.io.PrintStream; + +/** + * A cli command + */ +public interface CLICommand { + + /** + * Prints the description + * + * @param out the pront stream + * @param prefix the prefex string which should + * printed at the beginning of each line + */ + void printDescription(PrintStream out, String prefix); + + /** + * Esecuted the command + * + * @param args the arguments + * @throws CLIException CLIException + */ + void execute(String[] args) throws CLIException; +} diff --git a/src/main/java/de/neemann/digital/cli/CLIException.java b/src/main/java/de/neemann/digital/cli/CLIException.java new file mode 100644 index 000000000..3b848510e --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/CLIException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import java.io.PrintStream; + +/** + * he command line exception + */ +public class CLIException extends Exception { + private final int exitCode; + + /** + * Creates a new instance + * + * @param message the message + * @param exitCode the exit code + */ + public CLIException(String message, int exitCode) { + super(message); + this.exitCode = exitCode; + } + + /** + * Creates a new instance + * + * @param message the message + * @param cause the cause + */ + public CLIException(String message, Throwable cause) { + super(message, cause); + exitCode = 200; + } + + /** + * @return the exit code + */ + public int getExitCode() { + return exitCode; + } + + /** + * Pronts a error message to the stream + * + * @param out the print stream + */ + public void printMessage(PrintStream out) { + out.println(getMessage()); + Throwable c = getCause(); + if (c != null) { + if (c instanceof CLIException) + ((CLIException) c).printMessage(out); + else + out.println(c.getMessage()); + } + } +} diff --git a/src/main/java/de/neemann/digital/cli/CLITester.java b/src/main/java/de/neemann/digital/cli/CLITester.java new file mode 100644 index 000000000..66cdd31b1 --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/CLITester.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import de.neemann.digital.lang.Lang; +import de.neemann.digital.testing.CommandLineTester; + +import java.io.File; +import java.io.IOException; + +/** + * Used to test circuits + */ +public class CLITester extends SimpleCommand { + private final Argument circ; + private final Argument test; + + /** + * Creates a new CLI command + */ + public CLITester() { + super("test"); + circ = addArgument(new Argument<>("circ", "", false)); + test = addArgument(new Argument<>("test", "", true)); + } + + @Override + protected void execute() throws CLIException { + try { + CommandLineTester clt = new CommandLineTester(new File(circ.get())); + if (test.isSet()) + clt.useTestCasesFrom(new File(test.get())); + int errors = clt.execute(); + if (errors > 0) + throw new CLIException(Lang.get("cli_thereAreTestFailures"), errors); + } catch (IOException e) { + throw new CLIException(Lang.get("cli_errorExecutingTests"), e); + } + } +} diff --git a/src/main/java/de/neemann/digital/cli/Muxer.java b/src/main/java/de/neemann/digital/cli/Muxer.java new file mode 100644 index 000000000..3d2272c9c --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/Muxer.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import de.neemann.digital.lang.Lang; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.HashMap; + +/** + * The command muxer + */ +public class Muxer implements CLICommand { + /** + * The main muxer + */ + public static final Muxer MAIN_MUXER = new Muxer() + .addCommand(new CLITester()); + + private final HashMap commands; + private final String name; + + private Muxer() { + this("java -jar Digital.jar CLI"); + } + + /** + * Creates a new muxer + * + * @param name the name of the muxer + */ + public Muxer(String name) { + this.name = name; + this.commands = new HashMap<>(); + } + + /** + * Adds a command to the muxer + * + * @param command the command + * @return this for chained calls + */ + public Muxer addCommand(SimpleCommand command) { + return addCommand(command.getName(), command); + } + + /** + * Adds a command to the muxer + * + * @param name the name of the command + * @param command the command + * @return this for chained calls + */ + public Muxer addCommand(String name, CLICommand command) { + commands.put(name, command); + return this; + } + + @Override + public void printDescription(PrintStream out, String prefix) { + out.print(prefix); + out.print(name); + out.println(); + for (CLICommand c : commands.values()) + c.printDescription(out, prefix + " "); + } + + @Override + public void execute(String[] args) throws CLIException { + if (args.length == 0) + throw new CLIException(Lang.get("cli_notEnoughArgumentsGiven"), 100); + + CLICommand command = commands.get(args[0]); + if (command == null) + throw new CLIException(Lang.get("cli_command_N_hasNoSubCommand_N", name, args[0]), 101); + + command.execute(Arrays.copyOfRange(args, 1, args.length)); + } + +} diff --git a/src/main/java/de/neemann/digital/cli/SimpleCommand.java b/src/main/java/de/neemann/digital/cli/SimpleCommand.java new file mode 100644 index 000000000..074a32b96 --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/SimpleCommand.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import de.neemann.digital.lang.Lang; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +/** + * E simple executable command + */ +public abstract class SimpleCommand implements CLICommand { + private final String name; + private final ArrayList> arguments; + + /** + * Creates a new command + * + * @param name the name of the command + */ + public SimpleCommand(String name) { + this.name = name; + arguments = new ArrayList<>(); + } + + /** + * Adds an argument to the command + * + * @param argument the argument + * @param the type of the arguments value + * @param the type of the argument + * @return the argument itself + */ + public > A addArgument(A argument) { + arguments.add(argument); + return argument; + } + + /** + * @return the name of the argument + */ + public String getName() { + return name; + } + + @Override + public void printDescription(PrintStream out, String prefix) { + String message = Lang.get("cli_help_" + name); + out.print(prefix); + out.print(name); + for (Argument a : arguments) { + out.print(" "); + out.print(a); + } + out.println(":"); + + for (Argument a : arguments) + printString(out, prefix + " ", a + ":\t" + Lang.get("cli_help_" + name + "_" + a.getName())); + + printString(out, prefix + " ", message); + } + + void printString(PrintStream out, String prefix, String message) { + boolean lastWasSpace = false; + out.print(prefix); + int col = prefix.length(); + for (int i = 0; i < message.length(); i++) { + char c = message.charAt(i); + if (c == '\n') + c = ' '; + if (c != ' ' || !lastWasSpace) { + if (c == ' ') { + if (col > 70) { + out.print('\n'); + out.print(prefix); + col = prefix.length(); + } else { + out.print(c); + col++; + } + } else { + out.print(c); + col++; + } + } + lastWasSpace = c == ' '; + } + out.println(); + } + + @Override + public void execute(String[] args) throws CLIException { + int nonOptional = 0; + Iterator it = Arrays.asList(args).iterator(); + while (it.hasNext()) { + String n = it.next(); + if (n.startsWith("-")) { + if (!it.hasNext()) + throw new CLIException(Lang.get("cli_notEnoughArgumentsGiven"), 100); + set(n.substring(1), it.next()); + } else { + while (nonOptional < arguments.size() && arguments.get(nonOptional).isOptional()) { + nonOptional++; + } + if (nonOptional == arguments.size()) + throw new CLIException(Lang.get("cli_toMuchArguments"), 105); + + arguments.get(nonOptional).setString(n); + nonOptional++; + } + } + + for (Argument a : arguments) + if (!a.isOptional() && !a.isSet()) + throw new CLIException(Lang.get("cli_nonOptionalArgumentMissing_N", a), 105); + + execute(); + } + + /** + * Executes the command + * + * @throws CLIException CLIException + */ + protected abstract void execute() throws CLIException; + + private void set(String arg, String value) throws CLIException { + for (Argument a : arguments) + if (arg.equals(a.getName())) { + a.setString(value); + return; + } + throw new CLIException(Lang.get("cli_noArgument_N_available", arg), 104); + } + +} diff --git a/src/main/java/de/neemann/digital/cli/package-info.java b/src/main/java/de/neemann/digital/cli/package-info.java new file mode 100644 index 000000000..68097b1b6 --- /dev/null +++ b/src/main/java/de/neemann/digital/cli/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ + +/** + * The command line interface + */ +package de.neemann.digital.cli; diff --git a/src/main/java/package-info.java b/src/main/java/package-info.java new file mode 100644 index 000000000..80fbecf63 --- /dev/null +++ b/src/main/java/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ + +/* + * The command line interface + */ diff --git a/src/main/resources/lang/lang_de.xml b/src/main/resources/lang/lang_de.xml index 091625450..95fefac70 100644 --- a/src/main/resources/lang/lang_de.xml +++ b/src/main/resources/lang/lang_de.xml @@ -1541,6 +1541,24 @@ Sind evtl. die Namen der Variablen nicht eindeutig? RAM EEPROM + Es fehlt das nicht optionale Argument {0}. + Der Wert {0} ist kein bool. + Der Wert {0} ist keine Zahl. + Das Argument {0} fehlt. + Es sind nicht genug Argumente vorhanden. + Es gibt zu viele Argumente. + Ungültiger Typ. + Der Befehl {0} hat keinen Subbefehl {1}. + + Der erste Dateiname gibt die zu testende Schaltung an. + Wenn ein zweiter Dateiname angegeben wird, werden die Testfälle aus dieser Datei ausgeführt. + Wird kein zweiter Dateiname angegeben, werden die Tests aus der ersten Datei ausgeführt. + + Name der zu testenden Datei. + Name einer Datei mit Testfällen. + Es sind Tests fehlgeschlagen. + Es ist ein Fehler bei der Ausführung der Tests aufgetreten. + Fenster Über Digital Analyse diff --git a/src/main/resources/lang/lang_en.xml b/src/main/resources/lang/lang_en.xml index 3fe7aa9e2..d84fde1ef 100644 --- a/src/main/resources/lang/lang_en.xml +++ b/src/main/resources/lang/lang_en.xml @@ -1505,6 +1505,25 @@ RAM EEPROM + The non-optional argument {0} is missing. + The value {0} is no bool. + The value {0} is not a number. + The argument {0} is missing. + There are not enough arguments. + There are too many arguments. + Invalid type. + The command {0} has no sub-command {1}. + + The first file name specifies the circuit to be tested. + If a second file name is specified, the test cases are executed from this file. + If no second file name is specified, the tests are executed from the first file. + + Name of the file to be tested. + Name of a file with test cases. + Tests have failed. + An error has occurred during the execution of the tests. + + Windows About Analysis diff --git a/src/test/java/de/neemann/digital/cli/ArgumentTest.java b/src/test/java/de/neemann/digital/cli/ArgumentTest.java new file mode 100644 index 000000000..88aa79c06 --- /dev/null +++ b/src/test/java/de/neemann/digital/cli/ArgumentTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import junit.framework.TestCase; + +public class ArgumentTest extends TestCase { + + public void testString() throws CLIException { + Argument a = new Argument<>("n", "a", false); + assertEquals("a", a.get()); + a.setString("hello"); + assertEquals("hello", a.get()); + } + + public void testBool() throws CLIException { + Argument a = new Argument<>("n", true, false); + assertTrue(a.get()); + a.setString("false"); + assertFalse(a.get()); + a.setString("true"); + assertTrue(a.get()); + a.setString("0"); + assertFalse(a.get()); + a.setString("1"); + assertTrue(a.get()); + a.setString("no"); + assertFalse(a.get()); + a.setString("yes"); + assertTrue(a.get()); + + try { + a.setString("foo"); + fail(); + } catch (CLIException e) { + } + } + + public void testInteger() throws CLIException { + Argument a = new Argument<>("n", 2, false); + assertEquals(2, (int) a.get()); + a.setString("5"); + assertEquals(5, (int) a.get()); + + try { + a.setString("foo"); + fail(); + } catch (CLIException e) { + } + + } +} \ No newline at end of file diff --git a/src/test/java/de/neemann/digital/cli/SimpleCommandTest.java b/src/test/java/de/neemann/digital/cli/SimpleCommandTest.java new file mode 100644 index 000000000..b1443b83d --- /dev/null +++ b/src/test/java/de/neemann/digital/cli/SimpleCommandTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.cli; + +import junit.framework.TestCase; + +public class SimpleCommandTest extends TestCase { + + private static class TestCommand extends SimpleCommand { + private boolean wasExecuted; + + private TestCommand() { + super("test"); + } + + @Override + protected void execute() { + wasExecuted = true; + } + + public void testExecutes(boolean shouldBeExecuted) { + assertEquals(shouldBeExecuted, wasExecuted); + } + } + + public void testOptional() throws CLIException { + TestCommand tc = new TestCommand(); + Argument n1 = tc.addArgument(new Argument<>("n1", "", false)); + Argument n2 = tc.addArgument(new Argument<>("n2", "", false)); + + tc.execute(new String[]{"name1", "name2"}); + + assertEquals("name1", n1.get()); + assertEquals("name2", n2.get()); + } + + public void testOptional2() throws CLIException { + TestCommand tc = new TestCommand(); + Argument n1 = tc.addArgument(new Argument<>("n1", "", false)); + Argument n2 = tc.addArgument(new Argument<>("n2", "", false)); + + tc.execute(new String[]{"-n1", "name1", "-n2", "name2"}); + + assertEquals("name1", n1.get()); + assertEquals("name2", n2.get()); + } + + public void testOptional3() { + TestCommand tc = new TestCommand(); + Argument n1 = tc.addArgument(new Argument<>("n1", "", false)); + Argument n2 = tc.addArgument(new Argument<>("n2", "", false)); + + try { + tc.execute(new String[]{"name1"}); + fail(); + } catch (CLIException e) { + } + } + + public void testOptional4() throws CLIException { + TestCommand tc = new TestCommand(); + Argument n1 = tc.addArgument(new Argument<>("n1", "n1", true)); + Argument n2 = tc.addArgument(new Argument<>("n2", "n2", true)); + + tc.execute(new String[]{}); + assertEquals("n1", n1.get()); + assertEquals("n2", n2.get()); + } + + public void testOptional5() throws CLIException { + TestCommand tc = new TestCommand(); + Argument n1 = tc.addArgument(new Argument<>("n1", "n1", true)); + Argument n2 = tc.addArgument(new Argument<>("n2", "n2", true)); + + try { + tc.execute(new String[]{"test"}); + fail(); + } catch (CLIException e) { + } + } + + public void testWrongArgument() { + TestCommand tc = new TestCommand(); + Argument n1 = tc.addArgument(new Argument<>("n1", "", true)); + Argument n2 = tc.addArgument(new Argument<>("n2", "", true)); + + try { + tc.execute(new String[]{"-n3", "test"}); + fail(); + } catch (CLIException e) { + } + + try { + tc.execute(new String[]{"-n1"}); + fail(); + } catch (CLIException e) { + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/neemann/digital/lang/TestLang.java b/src/test/java/de/neemann/digital/lang/TestLang.java index 36331be2d..7ab0a9eb4 100644 --- a/src/test/java/de/neemann/digital/lang/TestLang.java +++ b/src/test/java/de/neemann/digital/lang/TestLang.java @@ -68,7 +68,7 @@ public class TestLang extends TestCase { StringBuilder sb = new StringBuilder(); for (String key : map.keySet()) { if (!keys.contains(key)) { - if (!(key.startsWith("key_") || key.startsWith("elem_") || key.startsWith("attr_panel_") || key.startsWith("tutorial"))) { + if (!(key.startsWith("key_") || key.startsWith("elem_") || key.startsWith("attr_panel_") || key.startsWith("tutorial") || key.startsWith("cli_help_"))) { if (sb.length() > 0) sb.append(", "); sb.append('"').append(key).append('"');