diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 5787f9dd1f..9da8bcd8ac 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -24,6 +24,8 @@ Further documentation can be found in: ## Version 5.9.1 (???, 2026) * Daikon and its tools now reject a malformed command-line option. +* All tools accept `-h` and `--help`, which print a usage message and exit + with status 0. * `daikon.diff.Diff` no longer accepts the `-u` and `-z` options, which had no effect. diff --git a/doc/daikon.texinfo b/doc/daikon.texinfo index 63824ae74e..ea54039cec 100644 --- a/doc/daikon.texinfo +++ b/doc/daikon.texinfo @@ -7034,6 +7034,9 @@ java -cp $@{DAIKONDIR@}/daikon.jar daikon.AnnotateNullable @i{[@var{flags}]} @va The flags are: @table @option +@item -h --help +Print usage message. + @item -n --nonnull-annotations Adds @samp{NonNull} annotations as well as @samp{Nullable} annotations. Unlike @samp{Nullable} annotations, @samp{NonNull} annotations are not guaranteed to be correct. diff --git a/java/daikon/AnnotateNullable.java b/java/daikon/AnnotateNullable.java index 5241c5e722..17769b8dfd 100644 --- a/java/daikon/AnnotateNullable.java +++ b/java/daikon/AnnotateNullable.java @@ -64,12 +64,24 @@ private AnnotateNullable() { @Option("-n Insert NonNull as well as Nullable annotations") public static boolean nonnull_annotations = false; + /** If true, print a usage message and exit. */ + @Option("-h Display usage information") + public static boolean help = false; + public static void main(String[] args) throws IOException { Options options = - new Options("plume.AnnotateNullable [options] ", AnnotateNullable.class); + new Options("daikon.AnnotateNullable [options] ", AnnotateNullable.class); String[] inv_files = options.parse(true, args); - assert inv_files.length == 1; + if (help) { + options.printUsage(); + System.exit(0); + } + if (inv_files.length != 1) { + System.err.println("must specify exactly one invariant file"); + options.printUsage(System.err); + System.exit(1); + } // Read the serialized invariant file File inv_file = new File(inv_files[0]); diff --git a/java/daikon/Chicory.java b/java/daikon/Chicory.java index c77e6426b5..eafad6653c 100644 --- a/java/daikon/Chicory.java +++ b/java/daikon/Chicory.java @@ -240,7 +240,7 @@ public static void main(String[] args) { public static void check_args(Options options, String[] target_args) { if (help) { options.printUsage(); - System.exit(1); + System.exit(0); } if (nesting_depth < 0) { System.out.printf("nesting depth (%d) must not be negative%n", nesting_depth); diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 26bf668530..0d2fb61b9b 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -4,6 +4,7 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; +import java.util.Arrays; import org.plumelib.util.ArraysPlume; /** @@ -42,6 +43,43 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String opterr = false; } + /** + * Processes the command-line arguments of a program that takes no options other than {@code -h} + * and {@code --help}. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments that are not options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + public static String[] nonOptionArgs(String[] argv, String usage) { + return argsAfterOptions(argv, "", usage); + } + + /** + * Processes command-line arguments, permitting no options other than {@code -h} and {@code + * --help}. + * + * @param argv the command-line arguments + * @param optstring "" to process all the arguments, or "+" to stop at the first non-option + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments that Getopt did not process as options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + private static String[] argsAfterOptions(String[] argv, String optstring, String usage) { + // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. + String[] argvCopy = argv.clone(); + DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); + // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. + int c = g.getopt(); + if (c != -1) { + throw new Daikon.BugInDaikon("getopt() returned " + c); + } + return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); + } + /** * Like {@link Getopt#getopt()}, but never returns {@code '?'}, {@code ':'}, or {@code 'h'}. * Getopt returns {@code ':'} for a missing argument if the optstring starts with {@code ':'}. diff --git a/java/daikon/DynComp.java b/java/daikon/DynComp.java index d7a4118c0a..3c03de694f 100644 --- a/java/daikon/DynComp.java +++ b/java/daikon/DynComp.java @@ -179,7 +179,7 @@ public static void main(String[] args) { public static void check_args(Options options, String[] targetArgs) { if (help) { options.printUsage(); - System.exit(1); + System.exit(0); } if (nesting_depth < 0) { System.out.printf("nesting depth (%d) must not be negative%n", nesting_depth); diff --git a/java/daikon/SplitDtrace.java b/java/daikon/SplitDtrace.java index d13b4b81cb..6a7ae08666 100644 --- a/java/daikon/SplitDtrace.java +++ b/java/daikon/SplitDtrace.java @@ -5,6 +5,7 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -29,6 +30,9 @@ */ public final class SplitDtrace { + /** The usage message for this program. */ + private static final String usage = "Usage: java daikon.SplitDtrace file.dtrace[.gz]"; + /** Do not instantiate. */ private SplitDtrace() { throw new UnsupportedOperationException("Do not instantiate"); @@ -40,14 +44,29 @@ private SplitDtrace() { * @param args one argument, the name of the .dtrace or .dtrace.gz file */ public static void main(String[] args) throws IOException { - if (args.length != 1) { - throw new RuntimeException( - "You must supply one argument which is the filename of the dtrace file"); + try { + mainHelper(args); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + } + } + + /** + * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is + * appropriate to be called programmatically. + * + * @param args command-line arguments, like those of {@link #main} + * @throws IOException if there is a problem reading or writing a file + */ + public static void mainHelper(String[] args) throws IOException { + String[] files = DaikonGetopt.nonOptionArgs(args, usage); + if (files.length != 1) { + throw new Daikon.UserError(usage); } - String filename = args[0].trim(); + String filename = files[0].trim(); boolean isGz = filename.endsWith(".dtrace.gz"); if (!filename.endsWith(".dtrace") && !isGz) { - throw new RuntimeException( + throw new Daikon.UserError( "Filename must end with .dtrace or .dtrace.gz: filename=" + filename); } int declNum = 1; @@ -56,6 +75,9 @@ public static void main(String[] args) throws IOException { ArrayList rec = new ArrayList<>(); while (true) { readRec(reader, rec); + if (rec.isEmpty()) { + throw new Daikon.UserError("No DECLARE record in " + filename); + } if (isDeclare(rec)) { break; } @@ -71,6 +93,8 @@ public static void main(String[] args) throws IOException { recNum++; } } + } catch (FileNotFoundException e) { + throw new Daikon.UserError(e, "File not found"); } System.out.println( diff --git a/java/daikon/dcomp/BuildJDK.java b/java/daikon/dcomp/BuildJDK.java index 7f1cae05f3..35cf4b0583 100644 --- a/java/daikon/dcomp/BuildJDK.java +++ b/java/daikon/dcomp/BuildJDK.java @@ -122,6 +122,10 @@ public static void main(String[] args) throws IOException { DynComp.class, DCInstrument.class); String[] cl_args = options.parse(true, args); + if (DynComp.help) { + options.printUsage(); + System.exit(0); + } if (cl_args.length < 1) { System.err.println("must specify destination dir"); options.printUsage(); diff --git a/java/daikon/dcomp/BuildJDK24.java b/java/daikon/dcomp/BuildJDK24.java index 74c83abacf..13fb445108 100644 --- a/java/daikon/dcomp/BuildJDK24.java +++ b/java/daikon/dcomp/BuildJDK24.java @@ -132,6 +132,10 @@ public static void main(String[] args) throws IOException { DynComp.class, DCInstrument24.class); String[] cl_args = options.parse(true, args); + if (DynComp.help) { + options.printUsage(); + System.exit(0); + } if (cl_args.length < 1) { System.err.println("must specify destination dir"); options.printUsage(); diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 0f50ea31e2..c1b3a2d428 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -9,6 +9,7 @@ import daikon.Daikon; import daikon.DaikonGetopt; import daikon.PrintInvariants; +import daikon.SplitDtrace; import daikon.UnionInvariants; import daikon.tools.DtraceDiff; import gnu.getopt.LongOpt; @@ -153,6 +154,7 @@ public void testPrintInvariantsUnrecognizedOption() { public void testHelpOption() { assertPrintsUsage(() -> DtraceDiff.mainHelper(new String[] {"--help"})); assertPrintsUsage(() -> UnionInvariants.mainHelper(new String[] {"--help"})); + assertPrintsUsage(() -> SplitDtrace.mainHelper(new String[] {"-h", "x"})); } /** diff --git a/java/daikon/tools/DtraceNonceFixer.java b/java/daikon/tools/DtraceNonceFixer.java index 942ed31dca..273e0de541 100644 --- a/java/daikon/tools/DtraceNonceFixer.java +++ b/java/daikon/tools/DtraceNonceFixer.java @@ -2,6 +2,7 @@ package daikon.tools; +import daikon.DaikonGetopt; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; @@ -53,14 +54,15 @@ public static void main(String[] args) { * @param args command-line arguments, like those of {@link #main} */ public static void mainHelper(final String[] args) { - if (args.length != 1) { + String[] files = DaikonGetopt.nonOptionArgs(args, usage); + if (files.length != 1) { throw new daikon.Daikon.UserError(usage); } String outputFilename = - args[0].endsWith(".gz") ? (args[0] + "_fixed.gz") : (args[0] + "_fixed"); + files[0].endsWith(".gz") ? (files[0] + "_fixed.gz") : (files[0] + "_fixed"); - try (BufferedReader br1 = FilesPlume.newBufferedFileReader(args[0]); + try (BufferedReader br1 = FilesPlume.newBufferedFileReader(files[0]); PrintWriter out1 = new PrintWriter(FilesPlume.newBufferedFileWriter(outputFilename))) { // maxNonce - the biggest nonce ever found in the file @@ -93,7 +95,7 @@ public static void mainHelper(final String[] args) { // now go back and add the OBJECT and CLASS invocations String allFixedFilename = - outputFilename.endsWith(".gz") ? (args[0] + "_all_fixed.gz") : (args[0] + "_all_fixed"); + outputFilename.endsWith(".gz") ? (files[0] + "_all_fixed.gz") : (files[0] + "_all_fixed"); try (BufferedReader br2 = FilesPlume.newBufferedFileReader(outputFilename); PrintWriter out2 = new PrintWriter(FilesPlume.newBufferedFileWriter(allFixedFilename))) { diff --git a/java/daikon/tools/ReadTrace.java b/java/daikon/tools/ReadTrace.java index b47369d795..994e354cd0 100644 --- a/java/daikon/tools/ReadTrace.java +++ b/java/daikon/tools/ReadTrace.java @@ -1,10 +1,13 @@ package daikon.tools; +import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.PptMap; import daikon.PptTopLevel; import daikon.ValueTuple; import daikon.VarInfo; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; @@ -32,6 +35,9 @@ */ public class ReadTrace { + /** The usage message for this program. */ + private static final String usage = "Usage: java daikon.tools.ReadTrace file1 file2 ..."; + /** Do not instantiate. */ private ReadTrace() { throw new UnsupportedOperationException("Do not instantiate"); @@ -43,12 +49,31 @@ private ReadTrace() { * @param args data trace file names, read from the command line */ public static void main(String[] args) { + try { + mainHelper(args); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + } + } + + /** + * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is + * appropriate to be called programmatically. + * + * @param args command-line arguments, like those of {@link #main} + */ + public static void mainHelper(String[] args) { + String[] traceFiles = DaikonGetopt.nonOptionArgs(args, usage); + if (traceFiles.length == 0) { + throw new Daikon.UserError("No trace files specified" + Daikon.lineSep + usage); + } CollectDataProcessor processor = new CollectDataProcessor(); PptMap ppts = new PptMap(); try { // `read_data_trace_files()` requires the list of files to be modifiable. - FileIO.read_data_trace_files(new ArrayList<>(Arrays.asList(args)), ppts, processor, false); - } catch (Exception e) { + FileIO.read_data_trace_files( + new ArrayList<>(Arrays.asList(traceFiles)), ppts, processor, false); + } catch (IOException e) { throw new Error(e); }