first working cli interface implementation

This commit is contained in:
hneemann 2020-06-08 16:23:57 +02:00
parent 39c21d1eac
commit 3c70b3a3bd
14 changed files with 710 additions and 1 deletions

32
src/main/java/CLI.java Normal file
View File

@ -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());
}
}
}

View File

@ -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 <T> the type of the argument
*/
public class Argument<T> {
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;
}
}

View File

@ -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;
}

View File

@ -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());
}
}
}

View File

@ -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<String> circ;
private final Argument<String> 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);
}
}
}

View File

@ -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<String, CLICommand> 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));
}
}

View File

@ -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<Argument<?>> 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 <T> the type of the arguments value
* @param <A> the type of the argument
* @return the argument itself
*/
public <T, A extends Argument<T>> 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<String> 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);
}
}

View File

@ -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;

View File

@ -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
*/

View File

@ -1541,6 +1541,24 @@ Sind evtl. die Namen der Variablen nicht eindeutig?</string>
<string name="lib_ram">RAM</string> <string name="lib_ram">RAM</string>
<string name="lib_eeprom">EEPROM</string> <string name="lib_eeprom">EEPROM</string>
<string name="cli_nonOptionalArgumentMissing_N">Es fehlt das nicht optionale Argument {0}.</string>
<string name="cli_notABool_N">Der Wert {0} ist kein bool.</string>
<string name="cli_notANumber_N">Der Wert {0} ist keine Zahl.</string>
<string name="cli_noArgument_N_available">Das Argument {0} fehlt.</string>
<string name="cli_notEnoughArgumentsGiven">Es sind nicht genug Argumente vorhanden.</string>
<string name="cli_toMuchArguments">Es gibt zu viele Argumente.</string>
<string name="cli_invalidType_N">Ungültiger Typ.</string>
<string name="cli_command_N_hasNoSubCommand_N">Der Befehl {0} hat keinen Subbefehl {1}.</string>
<string name="cli_help_test">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.
</string>
<string name="cli_help_test_circ">Name der zu testenden Datei.</string>
<string name="cli_help_test_test">Name einer Datei mit Testfällen.</string>
<string name="cli_thereAreTestFailures">Es sind Tests fehlgeschlagen.</string>
<string name="cli_errorExecutingTests">Es ist ein Fehler bei der Ausführung der Tests aufgetreten.</string>
<string name="menu_window">Fenster</string> <string name="menu_window">Fenster</string>
<string name="menu_about">Über Digital</string> <string name="menu_about">Über Digital</string>
<string name="menu_analyse">Analyse</string> <string name="menu_analyse">Analyse</string>

View File

@ -1505,6 +1505,25 @@
<string name="lib_ram">RAM</string> <string name="lib_ram">RAM</string>
<string name="lib_eeprom">EEPROM</string> <string name="lib_eeprom">EEPROM</string>
<string name="cli_nonOptionalArgumentMissing_N">The non-optional argument {0} is missing.</string>
<string name="cli_notABool_N">The value {0} is no bool.</string>
<string name="cli_notANumber_N">The value {0} is not a number.</string>
<string name="cli_noArgument_N_available">The argument {0} is missing.</string>
<string name="cli_notEnoughArgumentsGiven">There are not enough arguments.</string>
<string name="cli_toMuchArguments">There are too many arguments.</string>
<string name="cli_invalidType_N">Invalid type.</string>
<string name="cli_command_N_hasNoSubCommand_N">The command {0} has no sub-command {1}.</string>
<string name="cli_help_test">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.
</string>
<string name="cli_help_test_circ">Name of the file to be tested.</string>
<string name="cli_help_test_test">Name of a file with test cases.</string>
<string name="cli_thereAreTestFailures">Tests have failed.</string>
<string name="cli_errorExecutingTests">An error has occurred during the execution of the tests.</string>
<string name="menu_window">Windows</string> <string name="menu_window">Windows</string>
<string name="menu_about">About</string> <string name="menu_about">About</string>
<string name="menu_analyse">Analysis</string> <string name="menu_analyse">Analysis</string>

View File

@ -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<String> a = new Argument<>("n", "a", false);
assertEquals("a", a.get());
a.setString("hello");
assertEquals("hello", a.get());
}
public void testBool() throws CLIException {
Argument<Boolean> 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<Integer> 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) {
}
}
}

View File

@ -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<String> n1 = tc.addArgument(new Argument<>("n1", "", false));
Argument<String> 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<String> n1 = tc.addArgument(new Argument<>("n1", "", false));
Argument<String> 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<String> n1 = tc.addArgument(new Argument<>("n1", "", false));
Argument<String> 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<String> n1 = tc.addArgument(new Argument<>("n1", "n1", true));
Argument<String> 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<String> n1 = tc.addArgument(new Argument<>("n1", "n1", true));
Argument<String> 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<String> n1 = tc.addArgument(new Argument<>("n1", "", true));
Argument<String> 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) {
}
}
}

View File

@ -68,7 +68,7 @@ public class TestLang extends TestCase {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (String key : map.keySet()) { for (String key : map.keySet()) {
if (!keys.contains(key)) { 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) if (sb.length() > 0)
sb.append(", "); sb.append(", ");
sb.append('"').append(key).append('"'); sb.append('"').append(key).append('"');