Skip to content

Add a CLI #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b6df9f6
refactor: Start creating MiniCLI
MarcellPerger1 Jun 5, 2024
148e3ce
feat: Do most (not all yet) of the core CLI parsing stuff [WIP]
MarcellPerger1 Jun 6, 2024
003752a
feat: Finish `MiniCLI.Parser`
MarcellPerger1 Jun 7, 2024
0252b13
refactor: Fix some warnings
MarcellPerger1 Jun 7, 2024
dc6cb68
refactor: Add `MiniCLI.addBooleanOption`
MarcellPerger1 Jun 7, 2024
d968fda
feat: Add `MiniCLI.setPositionalArgCount`
MarcellPerger1 Jun 7, 2024
3efd55a
fix: Add checking for number of positional args
MarcellPerger1 Jun 11, 2024
634ca42
feat: Finish most of MiniCLI
MarcellPerger1 Jun 11, 2024
6fc0f92
fix: Fix bug
MarcellPerger1 Jun 11, 2024
5333ebe
fix: Make plain `-R` give an error
MarcellPerger1 Jun 13, 2024
33a6801
fix: Fix unused imports
MarcellPerger1 Jun 13, 2024
905cfc4
refactor: Add `Option.expect(Throwable)`
MarcellPerger1 Jun 14, 2024
555d6ce
fix: Clean up and fix code
MarcellPerger1 Jun 15, 2024
e1acca3
test: Start test
MarcellPerger1 Jun 15, 2024
3d37b20
Merge pull request #12 from MarcellPerger1/main
MarcellPerger1 Jun 16, 2024
38dc05e
test: Add more tests
MarcellPerger1 Jun 16, 2024
5ac9424
refactor: Add detail to error messages
MarcellPerger1 Jun 16, 2024
43603be
feat: Add support for `--` in CLI arguments
MarcellPerger1 Jun 20, 2024
0272fa0
refactor: Ensure that setting defaultIfNoValue also sets the valueMod…
MarcellPerger1 Jun 20, 2024
2f78522
refactor: .validate method, more tests
MarcellPerger1 Jun 20, 2024
95201e2
style: Prefer separate imports in non-test code
MarcellPerger1 Jun 20, 2024
e978ee9
feat: Finish CLI
MarcellPerger1 Jun 20, 2024
ae69ddf
fix: Make no positional args default to interactive
MarcellPerger1 Jun 20, 2024
1bc9308
feat: Add `runcli.sh`
MarcellPerger1 Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions runcli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
java -classpath ./target/classes/ net.marcellperger.mathexpr.cli.CLI "$@"
38 changes: 38 additions & 0 deletions src/main/java/net/marcellperger/mathexpr/IntRange.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
36 changes: 36 additions & 0 deletions src/main/java/net/marcellperger/mathexpr/UIntRange.java
Original file line number Diff line number Diff line change
@@ -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> T workaround(@NotNull Runnable r, T returnValue) {
r.run();
return returnValue;
}

}
72 changes: 72 additions & 0 deletions src/main/java/net/marcellperger/mathexpr/cli/CLI.java
Original file line number Diff line number Diff line change
@@ -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<Boolean> interactive;
CLIOption<String> 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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: this is indeed meh, need to figure out how to do this nicely here (Maybe ControlFlowBreak?)

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.marcellperger.mathexpr.cli.minicli;

import org.jetbrains.annotations.NotNull;

import java.util.List;

class BooleanCLIOption extends CLIOption<Boolean> {
public BooleanCLIOption(List<String> 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`
}
}
132 changes: 132 additions & 0 deletions src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIOption.java
Original file line number Diff line number Diff line change
@@ -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<T> {
Class<T> type;
List<String> names;
@NotNull Option<ValueMode> valueMode = Option.newNone();
@NotNull Option<T> defaultIfNoValue = Option.newNone();
@NotNull Option<T> defaultIfAbsent = Option.newNone();
T value;
boolean hasValue;

public CLIOption(Class<T> valueType, List<String> optionNames) {
names = optionNames;
type = valueType;
}

public String getDisplayName() {
return names.isEmpty() ? "<unnamed option>" : String.join("/", names.toArray(String[]::new));
}
public CLIParseException fmtNewParseExcWithName(@NotNull String fmt) {
return new CLIParseException(fmt.formatted(getDisplayName()));
}

@Contract(" -> this")
public CLIOption<T> setRequired() {
defaultIfAbsent = Option.newNone();
return this;
}
@Contract("_ -> this")
public CLIOption<T> setDefault(T defaultIfAbsent_) {
defaultIfAbsent = Option.newSome(defaultIfAbsent_);
return this;
}
@Contract("_ -> this")
public CLIOption<T> setDefaultIfNoValue(T defaultIfNoValue_) {
defaultIfNoValue = Option.newSome(defaultIfNoValue_);
return this;
}
public boolean isRequired() {
return defaultIfAbsent.isNone();
}

public List<String> 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<ValueMode> getDeclaredValueMode() {
return valueMode;
}
@Contract("_ -> this")
public CLIOption<T> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading