diff --git a/geequel-shell/src/main/java/org/neo4j/shell/Main.java b/geequel-shell/src/main/java/org/neo4j/shell/Main.java index 8f7a0e7..9fbb9b5 100644 --- a/geequel-shell/src/main/java/org/neo4j/shell/Main.java +++ b/geequel-shell/src/main/java/org/neo4j/shell/Main.java @@ -106,7 +106,7 @@ void startShell(@Nonnull CliArgs cliArgs) { try { CypherShell shell = new CypherShell(logger, prettyConfig); // Can only prompt for password if input has not been redirected - connectMaybeInteractively(shell, connectionConfig, isInputInteractive(), isOutputInteractive()); + connectMaybeInteractively(shell, connectionConfig, isInputInteractive(cliArgs), isOutputInteractive()); // Construct shell runner after connecting, due to interrupt handling ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, shell, logger, connectionConfig); diff --git a/geequel-shell/src/main/java/org/neo4j/shell/ShellRunner.java b/geequel-shell/src/main/java/org/neo4j/shell/ShellRunner.java index 3512b41..136c6b7 100644 --- a/geequel-shell/src/main/java/org/neo4j/shell/ShellRunner.java +++ b/geequel-shell/src/main/java/org/neo4j/shell/ShellRunner.java @@ -31,11 +31,10 @@ import org.neo4j.shell.parser.ShellStatementParser; import javax.annotation.Nonnull; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; import static org.fusesource.jansi.internal.CLibrary.STDIN_FILENO; import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; @@ -80,8 +79,9 @@ static ShellRunner getShellRunner(@Nonnull CliArgs cliArgs, return new InteractiveShellRunner(cypherShell, cypherShell, logger, new ShellStatementParser(), System.in, FileHistorian.getDefaultHistoryFile(), userMessagesHandler); } else { + InputStream inputStream = cliArgs.getFile().isPresent() ? Files.newInputStream(Paths.get(cliArgs.getFile().get())) : System.in; return new NonInteractiveShellRunner(cliArgs.getFailBehavior(), cypherShell, logger, - new ShellStatementParser(), System.in); + new ShellStatementParser(), inputStream); } } @@ -94,7 +94,7 @@ static boolean shouldBeInteractive(@Nonnull CliArgs cliArgs) { return false; } - return isInputInteractive(); + return isInputInteractive(cliArgs); } /** @@ -104,18 +104,18 @@ static boolean shouldBeInteractive(@Nonnull CliArgs cliArgs) { * @return true if the shell is reading from an interactive terminal, false otherwise (e.g., we are reading from a * file). */ - static boolean isInputInteractive() { + static boolean isInputInteractive(@Nonnull CliArgs cliArgs) { if (isWindows()) { // Input will never be a TTY on windows and it isatty seems to be able to block forever on Windows so avoid // calling it. - return System.console() != null; + return System.console() != null && !cliArgs.getFile().isPresent(); } try { return 1 == isatty(STDIN_FILENO); } catch (Throwable ignored) { // system is not using libc (like Alpine Linux) // Fallback to checking stdin OR stdout - return System.console() != null; + return System.console() != null && !cliArgs.getFile().isPresent(); } } diff --git a/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java b/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java index 16980af..183936a 100644 --- a/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java +++ b/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java @@ -91,11 +91,11 @@ public static CliArgs parse(@Nonnull String... args) { // Other arguments // geequel string might not be given, represented by null cliArgs.setCypher(ns.getString("geequel")); - // Fail behavior as sensible default and returns a proper type - cliArgs.setFailBehavior(ns.get("fail-behavior")); + + cliArgs.setFile(ns.getString("file")); //Set Output format - cliArgs.setFormat(Format.parse(ns.get("format"))); + cliArgs.setFormat(Format.parse(ns.get("format"), cliArgs)); cliArgs.setEncryption(ns.getBoolean("encryption")); @@ -111,6 +111,9 @@ public static CliArgs parse(@Nonnull String... args) { cliArgs.setDriverVersion(ns.getBoolean("driver-version")); + // Fail behavior as sensible default and returns a proper type + cliArgs.setFailBehavior(ns.get("fail-behavior")); + return cliArgs; } @@ -181,6 +184,10 @@ private static ArgumentParser setupParser() .help("print additional debug information") .action(new StoreTrueArgumentAction()); + parser.addArgument("-f", "--file") + .help("specify a file to run as a cypher script") + .setDefault(""); + parser.addArgument("--non-interactive") .help("force non-interactive mode, only useful if auto-detection fails (like on Windows)") .dest("force-non-interactive") diff --git a/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java b/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java index 69b7478..0c0e8bb 100644 --- a/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java +++ b/geequel-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java @@ -44,6 +44,7 @@ public class CliArgs { private boolean driverVersion = false; private int numSampleRows = DEFAULT_NUM_SAMPLE_ROWS; private boolean wrap = true; + private Optional file = Optional.empty(); /** * Set the scheme to the primary value, or if null, the fallback value. @@ -101,6 +102,13 @@ public void setCypher(@Nullable String cypher) { this.cypher = Optional.ofNullable(cypher); } + /** + * Set the file path to the cypher script to execute + */ + public void setFile (@Nullable String file) { + this.file = Optional.ofNullable(file); + } + /** * Set whether the connection should be encrypted */ @@ -156,6 +164,11 @@ public Optional getCypher() { return cypher; } + @Nonnull + public Optional getFile() { + return file; + } + @Nonnull public Format getFormat() { return format; diff --git a/geequel-shell/src/main/java/org/neo4j/shell/cli/Format.java b/geequel-shell/src/main/java/org/neo4j/shell/cli/Format.java index e8dd878..234e732 100644 --- a/geequel-shell/src/main/java/org/neo4j/shell/cli/Format.java +++ b/geequel-shell/src/main/java/org/neo4j/shell/cli/Format.java @@ -33,13 +33,13 @@ public enum Format { PLAIN; // TODO JSON, strictly intended for machine consumption with data formatted in JSON - public static Format parse(@Nonnull String format) { + public static Format parse(@Nonnull String format, @Nonnull CliArgs cliArgs) { if (format.equalsIgnoreCase(PLAIN.name())) { return PLAIN; } else if (format.equalsIgnoreCase( VERBOSE.name() )) { return VERBOSE; } else { - return isInputInteractive() && isOutputInteractive() ? VERBOSE : PLAIN; + return isInputInteractive(cliArgs) && isOutputInteractive() ? VERBOSE : PLAIN; } } } diff --git a/geequel-shell/src/test/java/org/neo4j/shell/CypherShellTest.java b/geequel-shell/src/test/java/org/neo4j/shell/CypherShellTest.java index c7b9b0b..370cacf 100644 --- a/geequel-shell/src/test/java/org/neo4j/shell/CypherShellTest.java +++ b/geequel-shell/src/test/java/org/neo4j/shell/CypherShellTest.java @@ -30,6 +30,7 @@ import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.shell.cli.CliArgHelper; import org.neo4j.shell.cli.CliArgs; +import org.neo4j.shell.cli.NonInteractiveShellRunner; import org.neo4j.shell.cli.StringShellRunner; import org.neo4j.shell.commands.CommandExecutable; import org.neo4j.shell.commands.CommandHelper; @@ -41,6 +42,8 @@ import org.neo4j.shell.state.BoltStateHandler; import org.neo4j.shell.state.ListBoltResult; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.util.Optional; @@ -281,6 +284,62 @@ public void specifyingACypherStringShouldGiveAStringRunner() throws IOException } } + + @Test + public void specifyingACypherStringShouldAlwaysGiveAStringRunner() throws IOException { + CliArgs cliArgs = CliArgHelper.parse("-f", "test-file", "MATCH (n) RETURN n "); + + ConnectionConfig connectionConfig = mock(ConnectionConfig.class); + + ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, offlineTestShell, logger, connectionConfig); + + if (!(shellRunner instanceof StringShellRunner)) { + fail("Expected a different runner than: " + shellRunner.getClass().getSimpleName()); + } + + cliArgs = CliArgHelper.parse("MATCH (n) RETURN n ", "-f", "test-file"); + + shellRunner = ShellRunner.getShellRunner(cliArgs, offlineTestShell, logger, connectionConfig); + + if (!(shellRunner instanceof StringShellRunner)) { + fail("Expected a different runner than: " + shellRunner.getClass().getSimpleName()); + } + } + + + @Test + public void specifyingAFilePathShouldGiveANonInteractiveRunner() throws IOException { + File file = File.createTempFile("test-file", ".cypher"); + file.deleteOnExit(); + FileOutputStream fos = new FileOutputStream(file); + fos.write("RETURN 1;".getBytes()); + fos.close(); + CliArgs cliArgs = CliArgHelper.parse("-f", file.getAbsolutePath()); + + ConnectionConfig connectionConfig = mock(ConnectionConfig.class); + + ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, offlineTestShell, logger, connectionConfig); + + if (!(shellRunner instanceof NonInteractiveShellRunner)) { + fail("Expected a different runner than: " + shellRunner.getClass().getSimpleName()); + } + } + + @Test + public void specifyingANonexistentFilePathShouldThrowAnError() throws IOException { + CliArgs cliArgs = CliArgHelper.parse("-f", "test-file"); + + ConnectionConfig connectionConfig = mock(ConnectionConfig.class); + + try { + ShellRunner.getShellRunner(cliArgs, offlineTestShell, logger, connectionConfig); + } catch (IOException e) { + assertEquals("java.nio.file.NoSuchFileException: test-file", e.toString()); + return; + } + fail("Expected an exception to be thrown"); + } + @Test public void setParameterDoesNotTriggerByBoltError() throws CommandException { // given diff --git a/geequel-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java b/geequel-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java index 154a091..cbb3953 100644 --- a/geequel-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java +++ b/geequel-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java @@ -117,4 +117,18 @@ public void setCypher() throws Exception { cliArgs.setCypher(null); assertFalse(cliArgs.getCypher().isPresent()); } + + @Test + public void setFile() throws Exception { + // default + assertFalse(cliArgs.getFile().isPresent()); + + cliArgs.setFile("foo"); + assertTrue(cliArgs.getFile().isPresent()); + //noinspection OptionalGetWithoutIsPresent + assertEquals("foo", cliArgs.getFile().get()); + + cliArgs.setFile(null); + assertFalse(cliArgs.getFile().isPresent()); + } }