diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java index 2abdea92d..d63480cd6 100644 --- a/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java +++ b/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java @@ -1,29 +1,217 @@ package io.nosqlbench.engine.cli; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +/** + *

Synopsis

+ * + * ArgsFile is a command-line modifier which can take linked list of + * command args and modify it, and/or modify argsfile refrenced in this way. + * + *

ArgsFile Selection

+ * + * During processing, any occurence of '-argsfile' selects the active argsfile and loads + * it into the command line in place of the '-argsfile' argument. By default the args file + * will be loaded if it exists, and a warning will be given if it does not. + * + * The '-argsfile-required <somepath>' version will throw an error if the args file + * is not present, but it will not report any warnings or details otherwise. + * + * The `-argsfile-optional <somepath> version will not throw an error if the args + * file is not present, and it will not report any warnings or details otherwise. + * + * A prefix command line can be given to ArgsFile to pre-load any settings. In this way + * it is possible to easily provide a default args file which will be loaded. For example, + * A prefix command of '-argsfile-optional <somepath>' will load options if they are + * available in the specified file, but will otherwise provide no feedback to the user. + * + *

ArgsFile Injection

+ * + * When an argsfile is loaded, it reads a command from each line into the current position + * of the command line. No parsing is done. Blank lines are ignored. Newlines are used as the + * argument delimiter, and lines that end with a backslash before the newline are automatically + * joined together. + * + *

ArgsFile Diagnostics

+ * + * All modifications to the command line should be reported to the logging facility at + * INFO level. This assumes that the calling layer wants to inform users of command line injections, + * and that the user can select to be notified of warnings only if desired. + * + *

Environment Variables

+ * + * Simple environment variable substitution is attempted for any pattern which appears as '$' followed + * by all uppercase letters and underscores. Any references of this type which are not resolvable + * will cause an error to be thrown. + */ public class ArgsFile { - private final Path argsPath; + private final static Logger logger = LoggerFactory.getLogger(ArgsFile.class); - public ArgsFile(String path) { - this.argsPath = Path.of(path); + private Path argsPath; + private LinkedList preload; + + public ArgsFile() { } - public LinkedList doArgsFile(String argsfileSpec, LinkedList arglist) { - return null; + public ArgsFile preload(String... preload) { + this.preload = new LinkedList(Arrays.asList(preload)); + return this; } - private LinkedList spliceArgs(String argsfileSpec, LinkedList arglist) { - Pattern envpattern = Pattern.compile("(?\\$[A-Za-z_]+)"); - Matcher matcher = envpattern.matcher(argsfileSpec); + private enum Selection { + // Ignore if not present, show injections at info + IgnoreIfMissing, + // Warn if not present, but show injections at info + WarnIfMissing, + // throw error if not present, show injections at info + ErrorIfMissing + } + + public LinkedList process(String... args) { + return process(new LinkedList(Arrays.asList(args))); + } + + public LinkedList process(LinkedList commandline) { + if (preload != null) { + LinkedList modified = new LinkedList(); + modified.addAll(preload); + modified.addAll(commandline); + preload = null; + commandline = modified; + } + LinkedList composed = new LinkedList<>(); + while (commandline.peekFirst() != null) { + String arg = commandline.peekFirst(); + switch (arg) { + case "-argsfile": + commandline.removeFirst(); + String argspath = readWordOrThrow(commandline, "path to an args file"); + setArgsFile(argspath, Selection.WarnIfMissing); + commandline = loadArgs(this.argsPath, Selection.WarnIfMissing, commandline); + break; + case "-argsfile-required": + commandline.removeFirst(); + String argspathRequired = readWordOrThrow(commandline, "path to an args file"); + setArgsFile(argspathRequired, Selection.ErrorIfMissing); + commandline = loadArgs(this.argsPath, Selection.ErrorIfMissing, commandline); + break; + case "-argsfile-optional": + commandline.removeFirst(); + String argspathOptional = readWordOrThrow(commandline, "path to an args file"); + setArgsFile(argspathOptional, Selection.IgnoreIfMissing); + commandline = loadArgs(this.argsPath, Selection.IgnoreIfMissing, commandline); + break; + case "-pin": + commandline.removeFirst(); + commandline = pinArg(commandline); + break; + case "-unpin": + commandline.removeFirst(); + commandline = unpinArg(commandline); + break; + default: + composed.addLast(commandline.removeFirst()); + } + + } + return composed; + } + + private LinkedList loadArgs(Path argspath, Selection mode, LinkedList commandline) { + if (!assertArgsFileExists(argspath, mode)) { + return commandline; + } + List lines = null; + try { + lines = Files.readAllLines(argspath); + } catch (IOException e) { + throw new RuntimeException(e); + } + List content = lines.stream() + .filter(s -> !s.startsWith("#")) + .filter(s -> !s.startsWith("/")) + .filter(s -> !s.isBlank()) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + StringBuilder splitword = new StringBuilder(); + LinkedList loaded = new LinkedList<>(); + for (String s : content) { + splitword.append(s); + if (!s.endsWith("\\")) { + loaded.addLast(splitword.toString()); + splitword.setLength(0); + } else { + splitword.setLength(splitword.length() - 1); + } + } + if (splitword.length() > 0) { + throw new RuntimeException("unqualified line continuation for '" + splitword.toString() + "'"); + } + + Iterator injections = loaded.descendingIterator(); + while (injections.hasNext()) { + String injection = injections.next(); + injection = injectEnv(injection); + commandline.addFirst(injection); + } + + return commandline; + } + + private boolean assertArgsFileExists(Path argspath, Selection mode) { + if (!Files.exists(argsPath)) { + switch (mode) { + case ErrorIfMissing: + throw new RuntimeException("A required argsfile was specified, but it does not exist: '" + argspath + "'"); + case WarnIfMissing: + logger.warn("An argsfile was specified, but it does not exist: '" + argspath + "'"); + case IgnoreIfMissing: + } + return false; + } + return true; + } + + private void setArgsFile(String argspath, Selection mode) { + this.argsPath = Path.of(argspath); +// assertIfMissing(this.argsPath,mode); + } + + private String readWordOrThrow(LinkedList commandline, String description) { + String found = commandline.peekFirst(); + if (found == null) { + throw new RuntimeException("Unable to read argument top option for " + description); + } + return commandline.removeFirst(); + } + + private LinkedList pinArg(LinkedList commandline) { + if (this.argsPath == null) { + throw new RuntimeException("No argsfile has been selected before using the pin option."); + } + return commandline; + } + + private LinkedList unpinArg(LinkedList commandline) { + if (this.argsPath == null) { + throw new RuntimeException("No argsfile has been selected before using the unpin option."); + } + return commandline; + } + + private String injectEnv(String word) { + Pattern envpattern = Pattern.compile("(?\\$[A-Z_]+)"); + Matcher matcher = envpattern.matcher(word); StringBuilder sb = new StringBuilder(); - while (matcher.find()) { String envvar = matcher.group("envvar"); String value = System.getenv(envvar); @@ -33,19 +221,9 @@ public class ArgsFile { matcher.appendReplacement(sb, value); } matcher.appendTail(sb); - Path argfilePath = Path.of(sb.toString()); - List lines = null; - try { - lines = Files.readAllLines(argfilePath); - } catch (IOException e) { - throw new RuntimeException(e); - } - // TODO: finish update logic here - return arglist; - + return sb.toString(); } - public LinkedList pin(LinkedList arglist) { return arglist; } diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java index 8c4457431..f0bd65e82 100644 --- a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java +++ b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java @@ -163,7 +163,8 @@ public class NBCLIOptions { } private LinkedList parseGlobalOptions(String[] args) { - ArgsFile argsfile = new ArgsFile(ARGS_FILE_DEFAULT); + ArgsFile argsfile = new ArgsFile(); + argsfile.preload("-argsfile-optional", ARGS_FILE_DEFAULT); LinkedList arglist = new LinkedList<>() {{ addAll(Arrays.asList(args)); @@ -188,18 +189,9 @@ public class NBCLIOptions { switch (word) { case ARGS_FILE: - arglist.removeFirst(); - String argsfileSpec = readWordOrThrow(arglist, "argsfile"); - argsfile = new ArgsFile(argsfileSpec); - arglist = argsfile.doArgsFile(argsfileSpec, arglist); - break; case ARGS_PIN: - arglist.removeFirst(); - arglist = argsfile.pin(arglist); - break; case ARGS_UNPIN: - arglist.removeFirst(); - arglist = argsfile.unpin(arglist); + arglist = argsfile.process(arglist); break; case ANNOTATE_EVENTS: arglist.removeFirst(); diff --git a/engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java b/engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java new file mode 100644 index 000000000..be6b11c59 --- /dev/null +++ b/engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java @@ -0,0 +1,39 @@ +package io.nosqlbench.engine.cli; + +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArgsFileTest { + + @Test + public void testLoadingArgs() { + LinkedList result; + ArgsFile argsFile = new ArgsFile(); + result = argsFile.process("-argsfile", "src/test/resources/argsfiles/nonextant.cli"); + assertThat(result).containsExactly(); + result = argsFile.process("-argsfile", "src/test/resources/argsfiles/alphagamma.cli"); + assertThat(result).containsExactly("alpha", "gamma"); + } + + @Test(expected = RuntimeException.class) + public void testLoadingMissingRequiredFails() { + LinkedList result; + ArgsFile argsFile = new ArgsFile(); + result = argsFile.process("-argsfile-required", "src/test/resources/argsfiles/nonextant.cli"); + } + + @Test + public void testLoadingInPlace() { + LinkedList result; + LinkedList commands = new LinkedList<>(List.of("--abc", "--def", "-argsfile", "src/test/resources/argsfiles/alphagamma.cli")); + ArgsFile argsFile = new ArgsFile().preload("-argsfile-optional", "src/test/resources/argsfiles/alphagamma.cli"); + result = argsFile.process(commands); + assertThat(result).containsExactly("alpha", "gamma", "--abc", "--def", "alpha", "gamma"); + + } + +} \ No newline at end of file diff --git a/engine-cli/src/test/resources/argsfiles/alphagamma.cli b/engine-cli/src/test/resources/argsfiles/alphagamma.cli new file mode 100644 index 000000000..febb53a79 --- /dev/null +++ b/engine-cli/src/test/resources/argsfiles/alphagamma.cli @@ -0,0 +1,2 @@ +alpha +gamma \ No newline at end of file