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

Reply via email to