-
Notifications
You must be signed in to change notification settings - Fork 0
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
MarcellPerger1
wants to merge
24
commits into
main
Choose a base branch
from
add-cli
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add a CLI #13
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 148e3ce
feat: Do most (not all yet) of the core CLI parsing stuff [WIP]
MarcellPerger1 003752a
feat: Finish `MiniCLI.Parser`
MarcellPerger1 0252b13
refactor: Fix some warnings
MarcellPerger1 dc6cb68
refactor: Add `MiniCLI.addBooleanOption`
MarcellPerger1 d968fda
feat: Add `MiniCLI.setPositionalArgCount`
MarcellPerger1 3efd55a
fix: Add checking for number of positional args
MarcellPerger1 634ca42
feat: Finish most of MiniCLI
MarcellPerger1 6fc0f92
fix: Fix bug
MarcellPerger1 5333ebe
fix: Make plain `-R` give an error
MarcellPerger1 33a6801
fix: Fix unused imports
MarcellPerger1 905cfc4
refactor: Add `Option.expect(Throwable)`
MarcellPerger1 555d6ce
fix: Clean up and fix code
MarcellPerger1 e1acca3
test: Start test
MarcellPerger1 3d37b20
Merge pull request #12 from MarcellPerger1/main
MarcellPerger1 38dc05e
test: Add more tests
MarcellPerger1 5ac9424
refactor: Add detail to error messages
MarcellPerger1 43603be
feat: Add support for `--` in CLI arguments
MarcellPerger1 0272fa0
refactor: Ensure that setting defaultIfNoValue also sets the valueMod…
MarcellPerger1 2f78522
refactor: .validate method, more tests
MarcellPerger1 95201e2
style: Prefer separate imports in non-test code
MarcellPerger1 e978ee9
feat: Finish CLI
MarcellPerger1 ae69ddf
fix: Make no positional args default to interactive
MarcellPerger1 1bc9308
feat: Add `runcli.sh`
MarcellPerger1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
java -classpath ./target/classes/ net.marcellperger.mathexpr.cli.CLI "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/net/marcellperger/mathexpr/cli/minicli/BooleanCLIOption.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
132
src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIOption.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
src/main/java/net/marcellperger/mathexpr/cli/minicli/CLIParseException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
?)