diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 637f013..c6332e8 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -3,6 +3,9 @@
+
+
+
diff --git a/runcli.sh b/runcli.sh
new file mode 100644
index 0000000..c3588b2
--- /dev/null
+++ b/runcli.sh
@@ -0,0 +1 @@
+java -classpath ./target/classes/ net.marcellperger.mathexpr.cli.CLI "$@"
diff --git a/src/main/java/net/marcellperger/mathexpr/IntRange.java b/src/main/java/net/marcellperger/mathexpr/IntRange.java
new file mode 100644
index 0000000..f94cacc
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/IntRange.java
@@ -0,0 +1,38 @@
+package net.marcellperger.mathexpr;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+public class IntRange {
+ int lo, hi;
+
+ protected IntRange(int min, int max, int ignoredMarker) {
+ if(min > max) throw new IllegalArgumentException("min must be grater than max");
+ lo = min;
+ hi = max;
+ }
+ public IntRange(@Nullable Integer min, @Nullable Integer max) {
+ this(Objects.requireNonNullElse(min, Integer.MIN_VALUE),
+ Objects.requireNonNullElse(max, Integer.MAX_VALUE), /*marker*/0);
+ }
+ public IntRange() {
+ this(null, null);
+ }
+
+ public int getMin() {
+ return lo;
+ }
+ public int getMax() {
+ return hi;
+ }
+
+ public boolean includes(int v) {
+ return lo <= v && v <= hi;
+ }
+
+ public String fancyRepr() {
+ if(lo == hi) return "exactly %d".formatted(lo);
+ return "%d to %d".formatted(lo, hi);
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/UIntRange.java b/src/main/java/net/marcellperger/mathexpr/UIntRange.java
new file mode 100644
index 0000000..1c051f7
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/UIntRange.java
@@ -0,0 +1,36 @@
+package net.marcellperger.mathexpr;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+public class UIntRange extends IntRange {
+ protected UIntRange(int min, int max, int ignoredMarker) {
+ super(workaround(() -> {
+ if(min < 0 || max < 0) throw new IllegalArgumentException();
+ }, min), max, ignoredMarker);
+ }
+
+ public UIntRange(@Nullable Integer min, @Nullable Integer max) {
+ this(Objects.requireNonNullElse(min, 0),
+ Objects.requireNonNullElse(max, Integer.MAX_VALUE), /*marker*/0);
+ }
+
+ public UIntRange() {
+ this(null, null);
+ }
+
+ /**
+ * Workaround to allow us to run code before super() call.
+ * This is required because java insists that super() be this first statement!
+ * So not even static stuff can be ran!
+ */
+ @Contract("_, _ -> param2")
+ private static T workaround(@NotNull Runnable r, T returnValue) {
+ r.run();
+ return returnValue;
+ }
+
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/CLI.java b/src/main/java/net/marcellperger/mathexpr/cli/CLI.java
new file mode 100644
index 0000000..bfc91a6
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/CLI.java
@@ -0,0 +1,72 @@
+package net.marcellperger.mathexpr.cli;
+
+import net.marcellperger.mathexpr.MathSymbol;
+import net.marcellperger.mathexpr.cli.minicli.CLIOption;
+import net.marcellperger.mathexpr.cli.minicli.CLIParseException;
+import net.marcellperger.mathexpr.cli.minicli.MiniCLI;
+import net.marcellperger.mathexpr.interactive.Shell;
+import net.marcellperger.mathexpr.parser.ExprParseException;
+import net.marcellperger.mathexpr.parser.Parser;
+import net.marcellperger.mathexpr.util.MathUtil;
+import net.marcellperger.mathexpr.util.Util;
+
+
+public class CLI {
+ MiniCLI cli;
+ CLIOption interactive;
+ CLIOption rounding;
+ Integer roundSf;
+
+ public CLI() {
+ cli = new MiniCLI();
+ interactive = cli.addBooleanOption("-i", "--interactive");
+ rounding = cli.addStringOption("-R", "--round-sf").setDefault("12");
+ cli.setPositionalArgCount(0, 1);
+ }
+
+ public void run(String[] args) {
+ try {
+ _run(args);
+ } catch (CLIParseException exc) {
+ System.err.println("Invalid CLI arguments: " + exc);
+ }
+ }
+ protected void _run(String[] args) {
+ parseArgs(args);
+ if(interactive.getValue() || cli.getPositionalArgs().isEmpty()) runInteractive(roundSf);
+ else runEvaluateExpr(roundSf);
+ }
+
+ private void parseArgs(String[] args) {
+ cli.parseArgs(args);
+ try {
+ roundSf = Integer.parseInt(rounding.getValue());
+ } catch (NumberFormatException e) {
+ throw new CLIParseException(e);
+ }
+ }
+
+ private void runEvaluateExpr(int roundSf) {
+ if(cli.getPositionalArgs().isEmpty())
+ throw new CLIParseException("1 argument expected without -i/--interactive");
+ MathSymbol sym;
+ try {
+ sym = new Parser(cli.getPositionalArgs().getFirst()).parse();
+ } catch (ExprParseException exc) {
+ // TODO this is a bit meh solution here
+ Util.throwAsUnchecked(exc); // Trick Java but I want call-site checked exceptions
+ return;
+ }
+ System.out.println(MathUtil.roundToSigFigs(sym.calculateValue(), roundSf));
+ }
+
+ private void runInteractive(int roundSf) {
+ if(!cli.getPositionalArgs().isEmpty())
+ throw new CLIParseException("No argument expected with -i/--interactive");
+ new Shell(roundSf).run();
+ }
+
+ public static void main(String[] args) {
+ new CLI().run(args);
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/minicli/BooleanCLIOption.java b/src/main/java/net/marcellperger/mathexpr/cli/minicli/BooleanCLIOption.java
new file mode 100644
index 0000000..bdafcee
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/minicli/BooleanCLIOption.java
@@ -0,0 +1,27 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+class BooleanCLIOption extends CLIOption {
+ public BooleanCLIOption(List optionNames) {
+ super(Boolean.class, optionNames);
+ setDefault(false);
+ setDefaultIfNoValue(true);
+ }
+
+ @Override
+ protected void _setValueFromString(@NotNull String s) {
+ setValue(switch (s.strip().toLowerCase()) {
+ case "0", "no", "false" -> false;
+ case "1", "yes", "true" -> true;
+ case String s2 -> throw fmtNewParseExcWithName("Bad boolean value '%s' for %%s".formatted(s2));
+ });
+ }
+
+ @Override
+ public boolean supportsSeparateValueAfterShortForm() {
+ return false; // cannot have `foo -r no` (use `-r=no` / `--long-form=no`
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIOption.java b/src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIOption.java
new file mode 100644
index 0000000..c63c1d7
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIOption.java
@@ -0,0 +1,132 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+import net.marcellperger.mathexpr.util.Util;
+import net.marcellperger.mathexpr.util.rs.Option;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+public abstract class CLIOption {
+ Class type;
+ List names;
+ @NotNull Option valueMode = Option.newNone();
+ @NotNull Option defaultIfNoValue = Option.newNone();
+ @NotNull Option defaultIfAbsent = Option.newNone();
+ T value;
+ boolean hasValue;
+
+ public CLIOption(Class valueType, List optionNames) {
+ names = optionNames;
+ type = valueType;
+ }
+
+ public String getDisplayName() {
+ return names.isEmpty() ? "" : String.join("/", names.toArray(String[]::new));
+ }
+ public CLIParseException fmtNewParseExcWithName(@NotNull String fmt) {
+ return new CLIParseException(fmt.formatted(getDisplayName()));
+ }
+
+ @Contract(" -> this")
+ public CLIOption setRequired() {
+ defaultIfAbsent = Option.newNone();
+ return this;
+ }
+ @Contract("_ -> this")
+ public CLIOption setDefault(T defaultIfAbsent_) {
+ defaultIfAbsent = Option.newSome(defaultIfAbsent_);
+ return this;
+ }
+ @Contract("_ -> this")
+ public CLIOption setDefaultIfNoValue(T defaultIfNoValue_) {
+ defaultIfNoValue = Option.newSome(defaultIfNoValue_);
+ return this;
+ }
+ public boolean isRequired() {
+ return defaultIfAbsent.isNone();
+ }
+
+ public List getNames() {
+ return names;
+ }
+
+ public T getValue() {
+ Util.realAssert(hasValue, "value should've been set before getValue() is called");
+ return value;
+ }
+
+ public void validate() {
+ switch (getValueMode()) {
+ case OPTIONAL, NONE -> defaultIfNoValue.expect(
+ new IllegalStateException("defaultIfNoValue must be provided for OPTIONAL/NONE valueModes"));
+ case REQUIRED -> {
+ if (defaultIfNoValue.isSome()) {
+ throw new IllegalStateException("defaultIfNoValue should not be specified with a REQUIRED valueMode");
+ }
+ }
+ }
+ }
+
+ public void begin() {
+ validate();
+ }
+ public void finish() {
+ if (!hasValue)
+ setValue(defaultIfAbsent.expect(fmtNewParseExcWithName("The %s option is required")));
+ }
+
+ public void setValue(T value_) {
+ value = value_;
+ hasValue = true;
+ }
+
+ // Nullable because we need a way to distinguish `--foo=''` and `--foo`
+ protected abstract void _setValueFromString(@NotNull String s);
+ public void setValueFromString(@Nullable String s) {
+ if(s == null) setValueFromNoValue();
+ else setValueFromString_hasValue(s);
+ }
+
+ public void setValueFromString_hasValue(@NotNull String s) {
+ Objects.requireNonNull(s);
+ getValueMode().validateHasValue(this, true);
+ _setValueFromString(s);
+ }
+ public void setValueFromNoValue() {
+ getValueMode().validateHasValue(this, false);
+ setValue(_expectGetDefaultIfNoValue());
+ }
+ protected T _expectGetDefaultIfNoValue() {
+ return defaultIfNoValue.expect(fmtNewParseExcWithName("The %s option requires a value"));
+ }
+
+ public ValueMode getDefaultValueMode() {
+ return defaultIfNoValue.isSome() ? ValueMode.OPTIONAL : ValueMode.REQUIRED;
+ }
+ public ValueMode getValueMode() {
+ return valueMode.unwrapOr(getDefaultValueMode());
+ }
+ public Option getDeclaredValueMode() {
+ return valueMode;
+ }
+ @Contract("_ -> this")
+ public CLIOption setValueMode(ValueMode mode) {
+ valueMode = Option.newSome(mode);
+ return this;
+ }
+
+ /**
+ * @return True if it supports `-b arg`, False to make `-b arg` into `-b`,`arg`
+ */
+ public boolean supportsSeparateValueAfterShortForm() {
+ return getValueMode() != ValueMode.NONE;
+ }
+
+ public void reset() {
+ value = null;
+ hasValue = false;
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIParseException.java b/src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIParseException.java
new file mode 100644
index 0000000..e4ed256
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIParseException.java
@@ -0,0 +1,18 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+public class CLIParseException extends IllegalArgumentException {
+ public CLIParseException() {
+ }
+
+ public CLIParseException(String s) {
+ super(s);
+ }
+
+ public CLIParseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public CLIParseException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/minicli/MiniCLI.java b/src/main/java/net/marcellperger/mathexpr/cli/minicli/MiniCLI.java
new file mode 100644
index 0000000..d678686
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/minicli/MiniCLI.java
@@ -0,0 +1,205 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+import net.marcellperger.mathexpr.UIntRange;
+import net.marcellperger.mathexpr.util.Pair;
+import net.marcellperger.mathexpr.util.Util;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+public class MiniCLI {
+ Map> options = new HashMap<>();
+ Set> optionSet = new HashSet<>();
+ UIntRange nPositionalArgs = new UIntRange();
+ List positionalArgs = new ArrayList<>();
+
+ public MiniCLI() {
+
+ }
+
+ public void setPositionalArgCount(UIntRange range) {
+ nPositionalArgs = range;
+ }
+ public void setPositionalArgCount(@Nullable Integer min, @Nullable Integer max) {
+ setPositionalArgCount(new UIntRange(min, max));
+ }
+ public void setPositionalArgCount(@Nullable /*null=any*/ Integer value) {
+ if(value == null) setPositionalArgCount(new UIntRange());
+ else setPositionalArgCount(value, value);
+ }
+
+ @Contract("_ -> new")
+ public CLIOption addStringOption(String @NotNull ... names) {
+ return addOptionFromFactory(names, StringCLIOption::new);
+ }
+ @Contract("_ -> new")
+ public CLIOption addBooleanOption(String @NotNull ... names) {
+ return addOptionFromFactory(names, BooleanCLIOption::new);
+ }
+
+ protected CLIOption addOptionFromFactory(
+ String @NotNull[] names,
+ Function super List, ? extends CLIOption> optionFactory) {
+ // First validate all of them so we don't mutate unless all are valid
+ for(String name : names) {
+ if (!name.startsWith("-")) throw new IllegalArgumentException("Option name must start with '-'");
+ if (options.containsKey(name)) {
+ throw new IllegalStateException("Argument '%s' has already been registered".formatted(name));
+ }
+ }
+ CLIOption opt = optionFactory.apply(List.of(names));
+ optionSet.add(opt);
+ for(String name : names) {
+ options.put(name, opt);
+ }
+ return opt;
+ }
+
+ public void reset() {
+ positionalArgs.clear();
+ optionSet.forEach(CLIOption::reset);
+ }
+
+ public void validate() {
+ optionSet.forEach(CLIOption::validate);
+ }
+
+ // TODO better positional arg handling (just dumping an array on the user is a bit meh)
+ public List getPositionalArgs() {
+ return positionalArgs;
+ }
+
+ @Contract("_ -> this")
+ public MiniCLI parseArgs(String[] args) {
+ return parseArgs(List.of(args));
+ }
+ @Contract("_ -> this")
+ public MiniCLI parseArgs(List args) {
+ reset();
+ new Parser(args).parse();
+ return this;
+ }
+ private @NotNull CLIOption> lookupOption(String opt) {
+ CLIOption> out = options.get(opt);
+ if(out == null) throw new CLIParseException("'%s' is not a valid option".formatted(opt));
+ return out;
+ }
+ private class Parser {
+ // NOTE: Do not strip ANYTHING! - the shell does that for us and it also removes quotes
+ @Nullable CLIOption> prev = null;
+ boolean finishedOptions = false;
+ List args;
+
+ public Parser(List args) {
+ this.args = args;
+ }
+
+ public void parse() {
+ validate();
+ begin();
+ pumpAllArgs();
+ finish();
+ }
+
+ public void pumpAllArgs() {
+ args.forEach(this::pumpSingleArg);
+ }
+
+ // Make this public as it could be useful for making stream-ing type stuff
+ public void pumpSingleArg(@NotNull String arg) {
+ if(finishedOptions) { // No more options so *must* be positional
+ addPositionalArg(arg);
+ return;
+ }
+ if(arg.equals("--")) {
+ finishedOptions = true;
+ return;
+ }
+ ArgType argT = ArgType.fromArg(arg);
+ if(prev != null && prev.getValueMode() == ValueMode.REQUIRED) {
+ // We NEED a value so force it, even if it looks like a flag
+ flushPrevWithValue(arg);
+ return;
+ }
+ switch (argT) {
+ case NORMAL -> {
+ if (prev != null) flushPrevWithValue(arg);
+ else addPositionalArg(arg);
+ }
+ case SINGLE -> {
+ if(prev != null) flushPrev();
+ if(!setFromKeyEqualsValue(arg)) {
+ prev = lookupOption(arg);
+ // flush immediately if next one cannot be a value
+ if(!prev.supportsSeparateValueAfterShortForm()) flushPrev();
+ }
+ }
+ case DOUBLE -> {
+ if(prev != null) flushPrev();
+ if(!setFromKeyEqualsValue(arg)) {
+ lookupOption(arg).setValueFromString(null);
+ }
+ }
+ }
+ }
+
+ public void begin() {
+ optionSet.forEach(CLIOption::begin);
+ }
+
+ public void finish() {
+ if(prev != null) flushPrev();
+ int nArgs = positionalArgs.size();
+ if(!nPositionalArgs.includes(nArgs))
+ throw new CLIParseException("Incorrect number of positional args (required %s, got %d)"
+ .formatted(nPositionalArgs.fancyRepr(), nArgs));
+ optionSet.forEach(CLIOption::finish); // Handle options
+ }
+
+ // Makes more logical sense reading the code
+ // (`if (![did]setFromKeyEqualValue(arg)) {...}` )
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean /*return success?*/ setFromKeyEqualsValue(String arg) {
+ if(!arg.contains("=")) return false;
+ Pair kv = Pair.ofArray(arg.split("=", 2));
+ CLIOption> opt = lookupOption(kv.left);
+ opt.setValueFromString(kv.right);
+ return true;
+ }
+
+ private void flushPrev() {
+ Util.realAssert(prev != null, "flushPrev must have a `prev` to flush");
+ prev.setValueFromString(null);
+ prev = null;
+ }
+ private void flushPrevWithValue(String value) {
+ Util.realAssert(prev != null, "flushPrevWithValue must have a `prev` to flush");
+ prev.setValueFromString(value);
+ prev = null;
+ }
+
+ private void addPositionalArg(String arg) {
+ int newSize = positionalArgs.size() + 1;
+ int maxArgc = nPositionalArgs.getMax();
+ if(newSize > maxArgc)
+ throw new CLIParseException("Too many positional args (expected max %d, got %d)".formatted(maxArgc, newSize));
+ positionalArgs.add(arg);
+ }
+ }
+ private enum ArgType {
+ NORMAL, SINGLE, DOUBLE;
+ public static ArgType fromArg(String arg) {
+ if(arg.startsWith("--")) return DOUBLE;
+ if(arg.startsWith("-")) return SINGLE;
+ return NORMAL;
+ }
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/minicli/StringCLIOption.java b/src/main/java/net/marcellperger/mathexpr/cli/minicli/StringCLIOption.java
new file mode 100644
index 0000000..5bbeba2
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/minicli/StringCLIOption.java
@@ -0,0 +1,16 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+class StringCLIOption extends CLIOption {
+ public StringCLIOption(List keys) {
+ super(String.class, keys);
+ }
+
+ @Override
+ protected void _setValueFromString(@NotNull String s) {
+ setValue(s);
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/cli/minicli/ValueMode.java b/src/main/java/net/marcellperger/mathexpr/cli/minicli/ValueMode.java
new file mode 100644
index 0000000..d44fbf4
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/cli/minicli/ValueMode.java
@@ -0,0 +1,31 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+/**
+ * Controls if a value is required for an option
+ */
+public enum ValueMode {
+ /**
+ * Specifying a value is not allowed (e.g. {@code --foo} is allowed, {@code --foo=76} is not)
+ */
+ NONE,
+ /**
+ * Specifying a value is optional (e.g. {@code --foo} and {@code --foo=76} are both allowed)
+ */
+ OPTIONAL,
+ /**
+ * Specifying a value is required (e.g. {@code --foo=76} is allowed, {@code --foo} is not)
+ */
+ REQUIRED,
+ ;
+ public void validateHasValue(CLIOption> opt, boolean hasValue) {
+ switch (this) {
+ case NONE -> {
+ if(hasValue) throw opt.fmtNewParseExcWithName("Specifying a value for the %s option is not allowed");
+ }
+ case OPTIONAL -> {}
+ case REQUIRED -> {
+ if(!hasValue) throw opt.fmtNewParseExcWithName("The %s option requires a value");
+ }
+ }
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/interactive/MathCommandHandler.java b/src/main/java/net/marcellperger/mathexpr/interactive/MathCommandHandler.java
index 83bda28..91c7f26 100644
--- a/src/main/java/net/marcellperger/mathexpr/interactive/MathCommandHandler.java
+++ b/src/main/java/net/marcellperger/mathexpr/interactive/MathCommandHandler.java
@@ -12,7 +12,7 @@ public class MathCommandHandler implements ShellCommandHandler {
public boolean run(String cmd, Shell sh) {
try {
MathSymbol sym = parseOrPrintError(cmd, sh);
- sh.out.println(MathUtil.roundToSigFigs(sym.calculateValue(), 12));
+ sh.out.println(MathUtil.roundToSigFigs(sym.calculateValue(), sh.roundToSf));
} catch (ControlFlowBreak _parseErrorAlreadyPrinted) {}
return true; // Always continue
}
diff --git a/src/main/java/net/marcellperger/mathexpr/interactive/Shell.java b/src/main/java/net/marcellperger/mathexpr/interactive/Shell.java
index 4f8621b..cb00650 100644
--- a/src/main/java/net/marcellperger/mathexpr/interactive/Shell.java
+++ b/src/main/java/net/marcellperger/mathexpr/interactive/Shell.java
@@ -9,14 +9,23 @@ public class Shell {
public Input in;
public PrintStream out;
ShellCommandParser commandParser;
+ int roundToSf;
public Shell() {
+ this(12);
+ }
+ public Shell(int roundToSf) {
in = new Input();
out = System.out;
commandParser = new ShellCommandParser(this);
+ this.roundToSf = roundToSf;
}
public static void main(String[] args) {
+ main();
+ }
+ @SuppressWarnings("ConfusingMainMethod")
+ public static void main() {
new Shell().run();
}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/Util.java b/src/main/java/net/marcellperger/mathexpr/util/Util.java
index 0e8ffe2..9cf2250 100644
--- a/src/main/java/net/marcellperger/mathexpr/util/Util.java
+++ b/src/main/java/net/marcellperger/mathexpr/util/Util.java
@@ -6,7 +6,13 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.*;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.SequencedCollection;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -108,17 +114,6 @@ public static void expectOrFail(boolean cond) {
if(!cond) throw new AssertionError("Assertion failed in expectOrFail");
}
-// @Contract("_, _ -> param1")
-// public static T expectOrFail(T value, @NotNull Predicate predicate, ) {
-// if(!predicate.test(value)) throw new AssertionError("Assertion failed in expectOrFail");
-// return value;
-// }
-// @Contract("_, true -> param1; _, false -> fail")
-// public static T expectOrFail(T value, boolean cond) {
-// if(!cond) throw new AssertionError("Assertion failed in expectOrFail");
-// return value;
-// }
-
@Contract("_, _ -> param1")
public static @NotNull T withCause(@NotNull T exc, @Nullable Throwable cause) {
if(cause != null) exc.initCause(cause);
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java b/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java
index aa6140d..88b96ef 100644
--- a/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java
@@ -1,5 +1,6 @@
package net.marcellperger.mathexpr.util.rs;
+import net.marcellperger.mathexpr.util.ThrowingSupplier;
import net.marcellperger.mathexpr.util.Util;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -143,6 +144,12 @@ default T expect(String msg) {
throw new OptionPanicException(msg);
});
}
+ default T expect(E excIfNone) throws E {
+ return switch (this) {
+ case None() -> throw excIfNone;
+ case Some(T value) -> value;
+ };
+ }
default T unwrap() {
return expect("Option.unwrap() got None value");
}
diff --git a/src/test/java/net/marcellperger/mathexpr/cli/minicli/MiniCLITest.java b/src/test/java/net/marcellperger/mathexpr/cli/minicli/MiniCLITest.java
new file mode 100644
index 0000000..83e486e
--- /dev/null
+++ b/src/test/java/net/marcellperger/mathexpr/cli/minicli/MiniCLITest.java
@@ -0,0 +1,139 @@
+package net.marcellperger.mathexpr.cli.minicli;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MiniCLITest {
+ @Nested
+ class ParseArgsE2E_MathexprCLI {
+ MiniCLI cli;
+ CLIOption rounding;
+ CLIOption interactive;
+
+ @BeforeEach
+ void setUp() {
+ cli = new MiniCLI();
+ rounding = cli.addStringOption("-R", "--round-sf").setDefault("12");
+ interactive = cli.addBooleanOption("-i", "--interactive");
+ cli.setPositionalArgCount(0, 1);
+ }
+
+ @Test
+ void testMathexprCLI() {
+ cli.parseArgs(new String[]{});
+ assertEquals("12", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of(), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"-i", "1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(true, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--interactive", "1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(true, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--interactive=false", "1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--interactive=true", "1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(true, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"-i=yes", "1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(true, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"-i=0", "1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"-i", "0"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(true, interactive.getValue());
+ assertEquals(List.of("0"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"-R", "6", "1+1 * 2"});
+ assertEquals("6", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{ "1+1 * 2", "-R", "6"});
+ assertEquals("6", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"-R", "-9", "1+1 * 2"});
+ assertEquals("-9", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--round-sf=99", "1+1 * 2"});
+ assertEquals("99", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("1+1 * 2"), cli.getPositionalArgs());
+
+ assertThrowsAndMsgContains(CLIParseException.class, "-R/--round-sf option requires a value",
+ () -> cli.parseArgs(new String[]{"1+1", "-R"}));
+ assertThrowsAndMsgContains(CLIParseException.class, "positional args",
+ () -> cli.parseArgs(new String[]{"1+2", "*8"}));
+ assertThrowsAndMsgContains(CLIParseException.class, "Bad boolean value 'abc' for -i/--interactive",
+ () -> cli.parseArgs(new String[]{"--interactive=abc"}));
+
+ }
+
+ @Test
+ void doubleDashHandling() {
+ cli.parseArgs(new String[]{"--", "-1+1 * 2"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("-1+1 * 2"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--", "-i"});
+ assertEquals("12", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("-i"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--round-sf=3", "--", "--"});
+ assertEquals("3", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("--"), cli.getPositionalArgs());
+ cli.parseArgs(new String[]{"--round-sf=3", "--", "--"});
+ assertEquals("3", rounding.getValue());
+ assertEquals(false, interactive.getValue());
+ assertEquals(List.of("--"), cli.getPositionalArgs());
+ assertThrowsAndMsgContains(CLIParseException.class, "positional args",
+ () -> cli.parseArgs(new String[]{"--", "--interactive", "6+2"}));
+ }
+ }
+
+ @Test
+ void test_miniCli_options_notParsing() {
+ {
+ MiniCLI cli = new MiniCLI();
+ cli.addStringOption("--abc", "-a", "-b");
+ assertThrowsAndMsgContains(IllegalStateException.class, "already been registered",
+ () -> cli.addBooleanOption("--qwerty", "-a"));
+ }
+ {
+ MiniCLI cli = new MiniCLI();
+ assertThrowsAndMsgContains(IllegalStateException.class,
+ "defaultIfNoValue should not be specified with a REQUIRED valueMode",
+ () -> {
+ cli.addStringOption("-a").setValueMode(ValueMode.REQUIRED).setDefaultIfNoValue("v");
+ cli.validate();
+ });
+ }
+ }
+
+ void assertThrowsAndMsgContains(@SuppressWarnings("SameParameterValue") Class cls,
+ String containsMsg, Executable fn) {
+ T exc = assertThrows(cls, fn);
+ assertTrue(exc.getMessage().contains(containsMsg), () ->
+ "Expected \"%s\" to contain \"%s\"".formatted(exc.getMessage(), containsMsg));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java b/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java
index 10fe2af..8f50740 100644
--- a/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java
+++ b/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java
@@ -227,6 +227,54 @@ void expect() {
assertNull(exc.getCause(), "Expected no cause for Option.expect()");
}
+ @SuppressWarnings("unused")
+ static class MyCustomException extends RuntimeException {
+ public MyCustomException() {}
+ public MyCustomException(String message) {
+ super(message);
+ }
+ public MyCustomException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public MyCustomException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ static class MyCustomCheckedException extends RuntimeException {
+ public MyCustomCheckedException() {}
+ public MyCustomCheckedException(String message) {
+ super(message);
+ }
+ public MyCustomCheckedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public MyCustomCheckedException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ @Test
+ void expect_exc() {
+ {
+ MyCustomException actualExc = new MyCustomException("unchecked exc");
+ assertEquals(314, assertDoesNotThrow(() -> getSome().expect(actualExc)));
+ Option err = getNone();
+ MyCustomException exc = assertThrows(
+ MyCustomException.class, () -> err.expect(actualExc));
+ assertEquals(exc, actualExc);
+ }
+ {
+ MyCustomCheckedException actualExc = new MyCustomCheckedException("checked exc");
+ assertEquals(314, assertDoesNotThrow(() -> getSome().expect(actualExc)));
+ Option err = getNone();
+ MyCustomCheckedException exc = assertThrows(
+ MyCustomCheckedException.class, () -> err.expect(actualExc));
+ assertEquals(exc, actualExc);
+ }
+ }
+
@Test
void unwrap() {
assertEquals(314, assertDoesNotThrow(() -> getSome().unwrap()));