From 4d794360939807d15d667d08dfbbd8c9be29eade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81rmin=20Scipiades?= Date: Sun, 4 Dec 2016 22:42:24 +0100 Subject: [PATCH] Add command line argument parsing Can load source files! --- build.gradle | 4 + example.prog | 1 + src/main/java/ppke/itk/xplang/ui/Main.java | 2 +- .../java/ppke/itk/xplang/ui/OptionParser.java | 48 +++++++++++ src/main/java/ppke/itk/xplang/ui/Program.java | 54 ++++++++++++- .../java/ppke/itk/xplang/ui/RunConfig.java | 34 ++++++++ .../ppke/itk/xplang/ui/OptionParserTest.java | 79 +++++++++++++++++++ src/test/resources/example.prog | 1 + 8 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 example.prog create mode 100644 src/main/java/ppke/itk/xplang/ui/OptionParser.java create mode 100644 src/main/java/ppke/itk/xplang/ui/RunConfig.java create mode 100644 src/test/java/ppke/itk/xplang/ui/OptionParserTest.java create mode 100644 src/test/resources/example.prog diff --git a/build.gradle b/build.gradle index 05037d5..150796d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ repositories { dependencies { compile group: 'com.github.stefanbirkner', name: 'fishbowl', version: '1.4.0' + compile group: 'net.sourceforge.argparse4j', name: 'argparse4j', version: '0.7.0' compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.5' compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.21' @@ -62,6 +63,9 @@ processResources { } task run(type: JavaExec) { + if(project.hasProperty("appArgs")) { + args Eval.me(appArgs) + } main = "ppke.itk.xplang.ui.Main" classpath = sourceSets.main.runtimeClasspath } diff --git a/example.prog b/example.prog new file mode 100644 index 0000000..3c2f667 --- /dev/null +++ b/example.prog @@ -0,0 +1 @@ +PROGRAM testing <_< >_> >_> >_> >_> program_vége diff --git a/src/main/java/ppke/itk/xplang/ui/Main.java b/src/main/java/ppke/itk/xplang/ui/Main.java index 2512f70..c6808ca 100644 --- a/src/main/java/ppke/itk/xplang/ui/Main.java +++ b/src/main/java/ppke/itk/xplang/ui/Main.java @@ -8,6 +8,6 @@ public class Main { */ public static void main(final String[] args) { Program program = new Program(); - program.run(); + program.run(args); } } diff --git a/src/main/java/ppke/itk/xplang/ui/OptionParser.java b/src/main/java/ppke/itk/xplang/ui/OptionParser.java new file mode 100644 index 0000000..c860d6a --- /dev/null +++ b/src/main/java/ppke/itk/xplang/ui/OptionParser.java @@ -0,0 +1,48 @@ +package ppke.itk.xplang.ui; + +import net.sourceforge.argparse4j.ArgumentParsers; +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +class OptionParser { + private static final Logger log = LoggerFactory.getLogger("Root.UI.ArgumentParser"); + + private final ArgumentParser parser; + + OptionParser() { + parser = ArgumentParsers.newArgumentParser("xplang", true) + .version(String.format("This is XPLanG version %s", Program.getVersion().describe())) + .defaultHelp(true); + + parser.addArgument("source") + .metavar("") + .type(Arguments.fileType().acceptSystemIn().verifyCanRead().verifyIsFile()) + .nargs(1) + .help("Source file"); + + parser.addArgument("--version") + .action(Arguments.version()); // FIXME this exists with a System.exit(0). That's sort of not ideal. + } + + RunConfig parseOptions(String[] args) { + log.debug("Parsing command line arguments: {}", Arrays.asList(args)); + + try { + Namespace res = parser.parseArgs(args); + List files = res.get("source"); + return new RunConfig(Program.Action.getDefaultAction(), files.get(0)); + } catch(ArgumentParserException e) { + log.error("Argument error: {}", e.getMessage()); + parser.handleError(e); + return new RunConfig(Program.Action.NONE, null); + } + } +} diff --git a/src/main/java/ppke/itk/xplang/ui/Program.java b/src/main/java/ppke/itk/xplang/ui/Program.java index 6e3063a..5d0fcd6 100644 --- a/src/main/java/ppke/itk/xplang/ui/Program.java +++ b/src/main/java/ppke/itk/xplang/ui/Program.java @@ -12,14 +12,36 @@ import ppke.itk.xplang.parser.Parser; import ppke.itk.xplang.util.VersionInfo; -import java.io.Reader; -import java.io.StringReader; +import java.io.*; +import java.nio.charset.StandardCharsets; class Program { private final static Logger log = LoggerFactory.getLogger("Root.UI"); private final static VersionInfo version = new VersionInfo(); + static VersionInfo getVersion() { + return version; + } + + /** + * Describes the course of action the program should take. + */ + enum Action { + /** The program should just call it a day and quit. */ + NONE, + + /** The program should perform a dry-run: analyse the source, but do nothing afterwards. */ + PARSE_ONLY, + + /** The program should take the source code, parse it, build up the AST, then execute it. */ + INTERPRET; + + static Action getDefaultAction() { + return INTERPRET; + } + }; + Program() { // empty ctor } @@ -27,13 +49,21 @@ class Program { /** * Run the program. */ - void run() { + void run(String[] args) { log.info("XPLanG starting"); log.info("OS: {}", System.getProperty("os.name")); log.info("Java: {}", System.getProperty("java.version")); log.info("Version: {}", version.describe()); - Reader source = new StringReader("PROGRAM testing <_< >_> >_> \n >_> >_> program_vége"); + OptionParser optionParser = new OptionParser(); + RunConfig run = optionParser.parseOptions(args); + + if(run.getAction() == Action.NONE) { + log.info("Exiting"); + return; + } + + Reader source = getSourceReader(run); ErrorLog errorLog = new ErrorLog(); Grammar grammar = new PlangGrammar(); @@ -52,6 +82,22 @@ void run() { interpreter.visit(root); } + private Reader getSourceReader(RunConfig run) { + Reader Result; + try { + if(run.getSourceFile().getName().equals("-")) { + Result = new InputStreamReader(System.in); + log.info("Opened stdin for reading"); + } else { + Result = new InputStreamReader(new FileInputStream(run.getSourceFile()), StandardCharsets.UTF_8); + log.info("Opened {} for reading", run.getSourceFile()); + } + } catch(FileNotFoundException e) { + throw new RuntimeException("Could not open source file for reading.", e); + } + return Result; + } + private void printErrors(ErrorLog errorLog) { for(CompilerMessage message : errorLog.getErrorMessages()) { System.out.println(message); diff --git a/src/main/java/ppke/itk/xplang/ui/RunConfig.java b/src/main/java/ppke/itk/xplang/ui/RunConfig.java new file mode 100644 index 0000000..2e6d669 --- /dev/null +++ b/src/main/java/ppke/itk/xplang/ui/RunConfig.java @@ -0,0 +1,34 @@ +package ppke.itk.xplang.ui; + +import java.io.File; + +/** + * Configures the program's behaviour. + */ +class RunConfig { + private final Program.Action action; + private final File sourceFile; + + RunConfig(Program.Action action, File sourceFile) { + this.action = action; + this.sourceFile = sourceFile; + } + + /** + * The course of action the program should take. + * @return + */ + Program.Action getAction() { + return action; + } + + /** + * Where is the source code the program should operate on? + * @return the File object representing the source code. The file is weakly guaranteed to exist and be readable. + * If the {@code name} property of the File is set to '-', that is a signal the program should read the + * source code from the standard input stream. + */ + File getSourceFile() { + return sourceFile; + } +} diff --git a/src/test/java/ppke/itk/xplang/ui/OptionParserTest.java b/src/test/java/ppke/itk/xplang/ui/OptionParserTest.java new file mode 100644 index 0000000..92994b9 --- /dev/null +++ b/src/test/java/ppke/itk/xplang/ui/OptionParserTest.java @@ -0,0 +1,79 @@ +package ppke.itk.xplang.ui; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class OptionParserTest { + private OptionParser parser; + + @Before + public void setUp() { + this.parser = new OptionParser(); + } + + @Test + public void getHelp() { + RunConfig config = parser.parseOptions(new String[]{"--help"}); + assertSame("Requesting the help screen should result in NONE action", + Program.Action.NONE, config.getAction() + ); + + config = parser.parseOptions(new String[]{"--help", "example.plang"}); + assertSame("The --help switch and a source file should result in NONE action", + Program.Action.NONE, config.getAction() + ); + } + + //@Test // FIXME don't run until --version does System.exit(1) + public void getVersion() { + RunConfig config = parser.parseOptions(new String[]{"--version"}); + assertSame("Requesting the version screen should result in NONE action", + Program.Action.NONE, config.getAction() + ); + + config = parser.parseOptions(new String[]{"--version", "example.prog"}); + assertSame("The --version switch and a source file should result in NONE action", + Program.Action.NONE, config.getAction() + ); + } + + @Test + public void specifyingSourceFiles() { + RunConfig config = parser.parseOptions(new String[]{"example.prog"}); + assertSame("Existing file as source input should be accepted, and should trigger the default action", + Program.Action.getDefaultAction(), config.getAction() + ); + assertEquals("Existing file as source input should be accepted.", + "example.prog", config.getSourceFile().getName() + ); + } + + @Test + public void stdInSource() { + RunConfig config = parser.parseOptions(new String[]{"-"}); + assertSame("- as source input should be accepted, and should trigger the default action", + Program.Action.getDefaultAction(), config.getAction() + ); + assertEquals("- as source input should be accepted (representing the standard input)", + new File("-"), config.getSourceFile() + ); + } + + @Test + public void errors() { + RunConfig config = parser.parseOptions(new String[]{"--invalid", "example.prog"}); + assertSame("Invalid options should result in a NONE action", + Program.Action.NONE, config.getAction() + ); + + config = parser.parseOptions(new String[]{"nonesuch.prog"}); + assertSame("Specifying nonexistent files should result in a NONE action", + Program.Action.NONE, config.getAction() + ); + } +} diff --git a/src/test/resources/example.prog b/src/test/resources/example.prog new file mode 100644 index 0000000..8482421 --- /dev/null +++ b/src/test/resources/example.prog @@ -0,0 +1 @@ +this file exists