This is an automated email from the ASF dual-hosted git repository. andy pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit 1df4833ff44755b198a176cf6174bd83849d6017 Author: Andy Seaborne <[email protected]> AuthorDate: Wed Nov 20 17:30:16 2024 +0000 GH-2862: Command line arguments from a file --- jena-cmds/pom.xml | 12 ++ .../java/org/apache/jena/cmd/ArgModuleGeneral.java | 6 +- .../src/main/java/org/apache/jena/cmd/Args.java | 136 +++++++++++++++++++++ .../java/org/apache/jena/cmd/CmdArgModule.java | 2 +- .../main/java/org/apache/jena/cmd/CmdGeneral.java | 1 - .../main/java/org/apache/jena/cmd/CmdLineArgs.java | 11 +- .../src/main/java/org/apache/jena/cmd/Usage.java | 91 +++++++------- .../java/org/apache/jena/cmds/TestCmdLine.java | 90 +++++++++++++- jena-cmds/testing/cmd/args-bad-1 | 2 + jena-cmds/testing/cmd/args-good-1 | 12 ++ jena-cmds/testing/cmd/args-good-2 | 5 + jena-cmds/testing/cmd/args-good-3 | 2 + 12 files changed, 308 insertions(+), 62 deletions(-) diff --git a/jena-cmds/pom.xml b/jena-cmds/pom.xml index 46e56aac95..b19565e4da 100644 --- a/jena-cmds/pom.xml +++ b/jena-cmds/pom.xml @@ -171,6 +171,18 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-suite-engine</artifactId> + <scope>test</scope> + </dependency> + </dependencies> <build> diff --git a/jena-cmds/src/main/java/org/apache/jena/cmd/ArgModuleGeneral.java b/jena-cmds/src/main/java/org/apache/jena/cmd/ArgModuleGeneral.java index 20aa017cd5..00da6b785e 100644 --- a/jena-cmds/src/main/java/org/apache/jena/cmd/ArgModuleGeneral.java +++ b/jena-cmds/src/main/java/org/apache/jena/cmd/ArgModuleGeneral.java @@ -19,6 +19,10 @@ package org.apache.jena.cmd; public interface ArgModuleGeneral extends ArgModule { - // Registration phase for usage messages + /** Registration phase for arguments, including usage messages */ public abstract void registerWith(CmdGeneral cmdLine); + + /** Processing phase after the command line has been parsed and the arguments present are known. */ + @Override + public default void processArgs(CmdArgModule cmdLine) {} } diff --git a/jena-cmds/src/main/java/org/apache/jena/cmd/Args.java b/jena-cmds/src/main/java/org/apache/jena/cmd/Args.java new file mode 100644 index 0000000000..31fd84fe5c --- /dev/null +++ b/jena-cmds/src/main/java/org/apache/jena/cmd/Args.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.cmd; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Args { + // ---- Arguments in a file. + + /** + * Pre-process the command line. + * <p> + * If the command line is "@file", then take the argument from the contents of the file. + * If there is no "@file", return the function parameter string array object. + * <p> + * The file format is: + * <ul> + * <li>One line per argument or argument value. Lines are striped of horizontal white space.</li> + * <li>Any "@file" in the file is not processed</li> + * <li>Lines can be "--argument", "--argument value". The line is tokenized at the first space after non-white space</li> + * <li>Using "--argument=value" is preferred.</li> + * <li>Arguments are ended by end-of-file, a line which is "--", or a line which does not start with "-". Subsequent lines are positional values.</li> + * <li>There is no escape mechanism. The arguments/argument-values are processed as if they appeared on the command line after shell escaping.</li> + * <li>Comments are lines with first non-whitespace character of {@code #} + * <li>Blank lines and lines of only horizontal white space are ignores</li> + * Example: + * <pre> + * # Example arguments file + * ## Another comment + * --arg1 + * --arg2 value + * --arg3=value + * # Use --arg= for an empty string value + * --empty= + * -q + * + * # Previous line ignored + * ## --notAnArgument + * </ul> + * @return The command line for argument and values. + */ + public static String[] argsPrepare(String[] argv) { + List<String> argsList = Arrays.asList(argv); + // Count! + List<String> indirects = argsList.stream().filter(s->s.startsWith("@")).toList(); + if ( indirects.isEmpty() ) + return argv; + if ( indirects.size() > 1 ) + throw new CmdException("Multiple arguments files"); + if ( argsList.size() > 1 ) + // @args and something else + throw new CmdException("Arguments file must be the only item on the command line"); + String indirect = indirects.get(0); + String fn = indirect.substring(1); + try { + if ( fn.isEmpty() ) + throw new CmdException("Empty arguments file name"); + Path path = Path.of(fn); + if ( ! Files.exists(path) ) + throw new CmdException("No such file: "+fn); + List<String> lines = Files.readAllLines(path); + String[] args2 = toArgsArray(lines); + return args2; + } catch (NoSuchFileException ex) { + throw new CmdException("No such file: "+fn); + } catch (IOException ex) { + ex.printStackTrace(); + throw new CmdException("Failed to process args file: "+ex.getMessage()); + } + } + + /** Convert the lines of the file to argument/value pairs. */ + private static String[] toArgsArray(List<String> lines) { + // Each line is "--arg" or "--arg SPC value" + List<String> outcome = new ArrayList<>(); + boolean positionalsStarted = false; + for ( String s : lines ) { + s = s.strip(); + if ( s.startsWith("#") ) + // Comment + continue; + if ( s.isEmpty() ) + continue; + if ( positionalsStarted ) { + outcome.add(s); + continue; + } + + if ( s.startsWith("@") ) + throw new CmdException("Argument file may not contain an argument file reference"); + if ( ! s.startsWith("-") ) { + // positional + //throw new CmdException("Command line in file does not start with a '-': "+s); + positionalsStarted = true; + outcome.add(s); + continue; + } + + // argument or argument and value + // Split on first space after argument, if any. + int idx = s.indexOf(' '); + if ( idx == -1 ) { + outcome.add(s.stripTrailing()); + continue; + } + + String a = s.substring(0,idx); + String v = s.substring(idx+1).strip(); + outcome.add(a); + outcome.add(v); + } + return outcome.toArray(new String[outcome.size()]); + } +} diff --git a/jena-cmds/src/main/java/org/apache/jena/cmd/CmdArgModule.java b/jena-cmds/src/main/java/org/apache/jena/cmd/CmdArgModule.java index bc13b9dde6..545e601bee 100644 --- a/jena-cmds/src/main/java/org/apache/jena/cmd/CmdArgModule.java +++ b/jena-cmds/src/main/java/org/apache/jena/cmd/CmdArgModule.java @@ -23,7 +23,7 @@ import java.util.List; public abstract class CmdArgModule extends CmdMain { - List<ArgModuleGeneral> modules = new ArrayList<>(); + private List<ArgModuleGeneral> modules = new ArrayList<>(); protected CmdArgModule(String[] argv) { super(argv); diff --git a/jena-cmds/src/main/java/org/apache/jena/cmd/CmdGeneral.java b/jena-cmds/src/main/java/org/apache/jena/cmd/CmdGeneral.java index f917302b71..6054b7c63c 100644 --- a/jena-cmds/src/main/java/org/apache/jena/cmd/CmdGeneral.java +++ b/jena-cmds/src/main/java/org/apache/jena/cmd/CmdGeneral.java @@ -27,7 +27,6 @@ public abstract class CmdGeneral extends CmdArgModule { protected ModGeneral modGeneral = new ModGeneral(this::printHelp); protected ModVersion modVersion = new ModVersion(true); - // Could be turned into a module but these are convenient as inherited flags protected CmdGeneral(String[] argv) { super(argv); diff --git a/jena-cmds/src/main/java/org/apache/jena/cmd/CmdLineArgs.java b/jena-cmds/src/main/java/org/apache/jena/cmd/CmdLineArgs.java index 1d42c4eff0..73192f5310 100644 --- a/jena-cmds/src/main/java/org/apache/jena/cmd/CmdLineArgs.java +++ b/jena-cmds/src/main/java/org/apache/jena/cmd/CmdLineArgs.java @@ -50,7 +50,7 @@ public class CmdLineArgs extends CommandLineBase { // ---- Setting the ArgDecls - /** Add an argument to those to be accepted on the command line. + /** Add an argument declaration sto those to be accepted on the command line. * @param argName Name * @param hasValue True if the command takes a (string) value * @return The command line processor object @@ -59,7 +59,7 @@ public class CmdLineArgs extends CommandLineBase { return add(new ArgDecl(hasValue, argName)); } - /** Add an argument to those to be accepted on the command line. + /** Add an argument declaration to those to be accepted on the command line. * Argument order reflects ArgDecl. * @param hasValue True if the command takes a (string) value * @param argName Name @@ -69,7 +69,7 @@ public class CmdLineArgs extends CommandLineBase { return add(new ArgDecl(hasValue, argName)); } - /** Add an argument object + /** Add an argument declaration * @param argDecl Argument to add * @return The command line processor object */ @@ -83,7 +83,7 @@ public class CmdLineArgs extends CommandLineBase { } /** - * Remove an argument and any values set for this argument. + * Remove an argument declaration and any values set for this argument. * @param argDecl Argument to remove * @return The command line processor object */ @@ -194,7 +194,7 @@ public class CmdLineArgs extends CommandLineBase { // ---- Indirection - static final String DefaultIndirectMarker = "@"; + static final String DefaultIndirectMarker = "^"; public boolean matchesIndirect(String s) { return matchesIndirect(s, DefaultIndirectMarker); } public boolean matchesIndirect(String s, String marker) { return s.startsWith(marker); } @@ -243,7 +243,6 @@ public class CmdLineArgs extends CommandLineBase { public boolean hasArg(ArgDecl argDecl) { return getArg(argDecl) != null; } - /** Get the argument associated with the argument declaration. * Actually returns the LAST one seen * @param argDecl Argument declaration to find diff --git a/jena-cmds/src/main/java/org/apache/jena/cmd/Usage.java b/jena-cmds/src/main/java/org/apache/jena/cmd/Usage.java index 766044313a..8713c3dd5e 100644 --- a/jena-cmds/src/main/java/org/apache/jena/cmd/Usage.java +++ b/jena-cmds/src/main/java/org/apache/jena/cmd/Usage.java @@ -27,7 +27,7 @@ import org.apache.jena.atlas.iterator.Iter; public class Usage { - public static class Category { + private static class Category { String desc; List<Entry> entries = new ArrayList<>(); Category(String desc) { @@ -35,60 +35,53 @@ public class Usage } } - private static class Entry { - String arg; - String msg; - Entry(String arg, String msg) { - this.arg = arg; - this.msg = msg; - } - } + private record Entry(String arg, String msg) {} - private List<Category> categories = new ArrayList<>(); + private List<Category> categories = new ArrayList<>(); - public Usage() { - // Start with an unnamed category - startCategory(null); - } + public Usage() { + // Start with an unnamed category + startCategory(null); + } - public void startCategory(String desc) { - categories.add(new Category(desc)); - } + public void startCategory(String desc) { + categories.add(new Category(desc)); + } - public void addUsage(String argName, String msg) { - current().entries.add(new Entry(argName, msg)); - } + public void addUsage(String argName, String msg) { + current().entries.add(new Entry(argName, msg)); + } - public void output(PrintStream out) { - output(new IndentedWriter(out)); - } + public void output(PrintStream out) { + output(new IndentedWriter(out)); + } - public void output(IndentedWriter out) { - int INDENT1 = 2; - int INDENT2 = 4; - out.incIndent(INDENT1); + public void output(IndentedWriter out) { + int INDENT1 = 2; + int INDENT2 = 4; + out.incIndent(INDENT1); - Iter.reverseIterate(categories, c->{ - if ( c.desc != null ) { - out.println(c.desc); - } - out.incIndent(INDENT2); - for ( final Entry e : c.entries ) { - out.print(e.arg); - if ( e.msg != null ) { - out.pad(20); - out.print(" "); - out.print(e.msg); - } - out.println(); - } - out.decIndent(INDENT2); - }); - out.decIndent(INDENT1); - out.flush(); - } + Iter.reverseIterate(categories, c->{ + if ( c.desc != null ) { + out.println(c.desc); + } + out.incIndent(INDENT2); + for ( final Entry e : c.entries ) { + out.print(e.arg); + if ( e.msg != null ) { + out.pad(20); + out.print(" "); + out.print(e.msg); + } + out.println(); + } + out.decIndent(INDENT2); + }); + out.decIndent(INDENT1); + out.flush(); + } - private Category current() { - return categories.get(categories.size() - 1); - } + private Category current() { + return categories.get(categories.size() - 1); + } } diff --git a/jena-cmds/src/test/java/org/apache/jena/cmds/TestCmdLine.java b/jena-cmds/src/test/java/org/apache/jena/cmds/TestCmdLine.java index 7555c552a1..43cd231c7b 100644 --- a/jena-cmds/src/test/java/org/apache/jena/cmds/TestCmdLine.java +++ b/jena-cmds/src/test/java/org/apache/jena/cmds/TestCmdLine.java @@ -18,20 +18,29 @@ package org.apache.jena.cmds; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.util.Arrays; import java.util.Iterator; -import org.junit.Test; - import org.apache.jena.cmd.Arg; +import org.apache.jena.cmd.Args; import org.apache.jena.cmd.ArgDecl; import org.apache.jena.cmd.CmdException; import org.apache.jena.cmd.CmdLineArgs; public class TestCmdLine { + + private static String DIR = "testing/cmd/"; + @Test public void test_Simple1() { String args[] = {""}; @@ -117,7 +126,7 @@ public class TestCmdLine { } - @Test(expected = CmdException.class) + @Test public void test_removeArg1() { String args[] = {"--arg=V1", "-v"}; CmdLineArgs cl = new CmdLineArgs(args); @@ -125,6 +134,79 @@ public class TestCmdLine { cl.add(argA); cl.removeArg(argA); // Exception. - cl.process(); + assertThrows(CmdException.class, ()->cl.process()); + } + + + @Test + public void args_no_file_1() { + String[] args = {}; + testArgsFileGood(args, new String[0]); } + + @Test + public void args_no_file_2() { + String[] args = {"-q", "-v", "positional"}; + testArgsFileGood(args, "-q", "-v", "positional"); + } + + @Test + public void args_file_01() { + String[] args = {"@"+DIR+"args-good-1"}; + testArgsFileGood(args, "--arg", "--arg1", "value1", "--arg2=value2", "--empty=", "--trailingspaces", "-q", "positional 1", "positional 2"); + } + + @Test + public void args_file_02() { + String[] args = {"@"+DIR+"args-good-2"}; + testArgsFileGood(args, "-arg", "--", "positional"); + } + + @Test + public void args_file_03() { + String[] args = {"@"+DIR+"args-good-3"}; + testArgsFileGood(args, "text"); + } + + + @Test + public void args_file_bad_01() { + String[] args = {"@"+DIR+"args-good-1", "--another"}; + testArgsFileBad(args); + } + + @Test + public void args_file_bad_02() { + String[] args = {"@"+DIR+"args-good-1", "@"+DIR+"args-good-2"}; + testArgsFileBad(args); + } + + @Test + public void args_file_bad_03() { + String[] args = {"@"}; + testArgsFileBad(args); + } + + @Test + public void args_file_bad_04() { + String[] args = {"@ filename"}; + testArgsFileBad(args); + } + + @Test + public void args_file_bad_file_01() { + String[] args = {"@"+DIR+"args-bad-1"}; + testArgsFileBad(args); + } + + private void testArgsFileGood(String[] args, String...expected) { + String[] args2 = Args.argsPrepare(args); + //assertArrayEquals(expected, args2, ()->{ return "Expected: "+Arrays.asList(expected)+" Got: "+Arrays.asList(args2)}); + assertArrayEquals(expected, args2, ()->("Expected: "+Arrays.asList(expected)+" Got: "+Arrays.asList(args2))); + } + + private void testArgsFileBad(String[] args) { + assertThrows(CmdException.class, ()->Args.argsPrepare(args)); + } + } diff --git a/jena-cmds/testing/cmd/args-bad-1 b/jena-cmds/testing/cmd/args-bad-1 new file mode 100644 index 0000000000..b540d783ff --- /dev/null +++ b/jena-cmds/testing/cmd/args-bad-1 @@ -0,0 +1,2 @@ +# Bad argument file. +@another diff --git a/jena-cmds/testing/cmd/args-good-1 b/jena-cmds/testing/cmd/args-good-1 new file mode 100644 index 0000000000..7eecbdaa60 --- /dev/null +++ b/jena-cmds/testing/cmd/args-good-1 @@ -0,0 +1,12 @@ +# Comment. First blank line has zero chars the second has 4 spaces +--arg + +--arg1 value1 +--arg2=value2 +--empty= + + --trailingspaces +-q + # comment +positional 1 +positional 2 diff --git a/jena-cmds/testing/cmd/args-good-2 b/jena-cmds/testing/cmd/args-good-2 new file mode 100644 index 0000000000..5017ecff7c --- /dev/null +++ b/jena-cmds/testing/cmd/args-good-2 @@ -0,0 +1,5 @@ +-arg +-- +positional + + diff --git a/jena-cmds/testing/cmd/args-good-3 b/jena-cmds/testing/cmd/args-good-3 new file mode 100644 index 0000000000..9c9cfbffa5 --- /dev/null +++ b/jena-cmds/testing/cmd/args-good-3 @@ -0,0 +1,2 @@ +# A word, no leading - +text
