From 04303948775fa68e6338b7e027da3ff41018081a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 5 Oct 2026 20:51:52 -0700 Subject: [PATCH 01/15] Exit with non-zero status on an unrecognized command-line option Previously, Daikon printed an error and the usage message but exited with status 0, so scripts such as runcluster.pl could not detect the failure. --- java/daikon/Daikon.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 1e7c1e1b73..03718c0bde 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1392,9 +1392,9 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { break; // case '?': - // break; // getopt() already printed an error + // getopt() already printed an error System.out.println(usage); - throw new Daikon.NormalTermination(); + throw new Daikon.UserError("Bad argument"); // default: throw new Daikon.BugInDaikon("getopt() returned " + c); From b27408038e3916432d8157419d07dee1877715ed Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 5 Oct 2026 21:20:14 -0700 Subject: [PATCH 02/15] Make all tools reject bad command-line options with a non-zero exit status Add Daikon.badOptionError, which names the bad option and suggests --help instead of printing the full usage message. Use it in every tool that uses getopt. --- java/daikon/Daikon.java | 34 +++++++++++-- java/daikon/MergeInvariants.java | 3 +- java/daikon/PrintInvariants.java | 2 +- java/daikon/UnionInvariants.java | 2 +- java/daikon/diff/Diff.java | 4 +- java/daikon/test/AllTestsSuite.java | 1 + java/daikon/test/BadOptionTest.java | 51 +++++++++++++++++++ java/daikon/tools/DtraceDiff.java | 3 +- java/daikon/tools/ExtractConsequent.java | 2 +- java/daikon/tools/InvariantChecker.java | 2 +- java/daikon/tools/compare/LogicalCompare.java | 3 +- java/daikon/tools/jtb/Annotate.java | 2 +- java/daikon/tools/jtb/CreateSpinfo.java | 2 +- 13 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 java/daikon/test/BadOptionTest.java diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 03718c0bde..ebfc12022b 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1392,9 +1392,7 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { break; // case '?': - // getopt() already printed an error - System.out.println(usage); - throw new Daikon.UserError("Bad argument"); + throw badOptionError(g, args); // default: throw new Daikon.BugInDaikon("getopt() returned " + c); @@ -1497,6 +1495,36 @@ public static String getOptarg(Getopt g) { return result; } + /** + * Returns an exception to throw when {@code g.getopt()} returns {@code '?'}, which indicates an + * unrecognized, ambiguous, or malformed command-line option. Getopt has already printed a message + * describing the problem. + * + * @param g a command-line argument processor whose {@code getopt()} just returned {@code '?'} + * @param args the command-line arguments that {@code g} is processing + * @return an exception that names the bad command-line argument + */ + public static UserError badOptionError(Getopt g, String[] args) { + int optopt = g.getOptopt(); + int optind = g.getOptind(); + String badOption; + if (optopt != 0) { + // A short option. It might be in the middle of an argument such as "-hx", so use optopt. + badOption = "-" + (char) optopt; + } else if (0 < optind && optind <= args.length) { + // A long option. Getopt has already advanced optind past it. + badOption = args[optind - 1]; + } else { + badOption = null; + } + return new UserError( + "Bad command-line option" + + (badOption == null ? "" : " " + badOption) + + "; run with --" + + help_SWITCH + + " for usage"); + } + /** * Invariants passed on the command line with the {@code --user_defined_invariant} option. A list * of class names in the format required by {@link Class#forName(String)}. diff --git a/java/daikon/MergeInvariants.java b/java/daikon/MergeInvariants.java index bc2009da5d..a0fbc9f777 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -161,8 +161,7 @@ public static void mainHelper(String[] args) break; case '?': - break; // getopt() already printed an error - + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index f7bc9121c3..0f6786fbf1 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -437,7 +437,7 @@ public static void mainHelper(String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - break; // getopt() already printed an error + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index ea34b0f075..7b32d6874d 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -91,7 +91,7 @@ public static void mainHelper(String[] args) throws Exception { break; // case '?': - break; // getopt() already printed an error + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index 0019d1e935..ecc2406233 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -319,9 +319,7 @@ public static void mainHelper(final String[] args) logging = true; break; case '?': - // getopt() already printed an error - System.out.println(usage); - throw new Daikon.UserError("Bad argument"); + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/test/AllTestsSuite.java b/java/daikon/test/AllTestsSuite.java index 7d74e7e7ac..9a817b266f 100644 --- a/java/daikon/test/AllTestsSuite.java +++ b/java/daikon/test/AllTestsSuite.java @@ -7,6 +7,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ daikon.chicory.RuntimeTest.class, + daikon.test.BadOptionTest.class, daikon.test.TestClassOrInterfaceTypeDecorateVisitor.class, daikon.test.TestAst.class, daikon.test.config.ConfigurationTest.class, diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java new file mode 100644 index 0000000000..66eb9f553e --- /dev/null +++ b/java/daikon/test/BadOptionTest.java @@ -0,0 +1,51 @@ +package daikon.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import daikon.Daikon; +import daikon.PrintInvariants; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; + +/** Tests that Daikon tools reject bad command-line options with a UserError. */ +public class BadOptionTest { + + /** + * Asserts that running {@code mainCall} throws a UserError that names the given option. + * + * @param badOption the bad option, which the error message should name + * @param mainCall a call to a tool's {@code mainHelper} method + */ + private static void assertBadOption(String badOption, ThrowingRunnable mainCall) { + Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); + assertEquals( + "Bad command-line option " + badOption + "; run with --help for usage", + String.valueOf(e.getMessage())); + } + + /** Tests an unrecognized long option. */ + @Test + public void testDaikonUnrecognizedLongOption() { + assertBadOption("--bogus", () -> Daikon.mainHelper(new String[] {"--bogus"})); + } + + /** Tests an unrecognized short option that is combined with a valid one. */ + @Test + public void testDaikonUnrecognizedShortOption() { + assertBadOption("-x", () -> Daikon.mainHelper(new String[] {"-xh"})); + } + + /** Tests a long option that is missing its required argument. */ + @Test + public void testDaikonMissingArgument() { + assertBadOption("--config", () -> Daikon.mainHelper(new String[] {"--config"})); + } + + /** Tests an unrecognized long option in a tool other than Daikon. */ + @Test + public void testPrintInvariantsUnrecognizedOption() { + assertBadOption( + "--bogus", () -> PrintInvariants.mainHelper(new String[] {"--bogus", "foo.inv.gz"})); + } +} diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 7d158ccc29..c553441b79 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -233,8 +233,7 @@ public static void mainHelper(final String[] args) { throw new Daikon.NormalTermination(); case '?': - break; // getopt() already printed an error - + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index f2ecf3caf3..c735960549 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -146,7 +146,7 @@ public static void mainHelper(final String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - break; // getopt() already printed an error + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index 70a3c804d1..734efb2d4b 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -186,7 +186,7 @@ public static void mainHelper(final String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - break; // getopt() already printed an error + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index bbbc6df993..0b63a306a4 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -734,8 +734,7 @@ public static void mainHelper(final String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - // getopt() already printed an error - throw new Daikon.UserError("Bad argument"); + throw Daikon.badOptionError(g, args); } } diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 9635cc5912..6374408f88 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -201,7 +201,7 @@ public static void mainHelper(final String[] args) throws Exception { slashslash = true; break; case '?': - break; // getopt() already printed an error + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index 56f1b154e2..5dd0a86213 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -113,7 +113,7 @@ public static void mainHelper(final String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - break; // getopt() already printed an error + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; From aa9fb706b5588d904df246991d9da62437d75141 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 5 Oct 2026 21:23:26 -0700 Subject: [PATCH 03/15] Reject bad options in test drivers; fix -h in DtraceDiff and SampleTester SampleTester and the generated SplitterFactoryTest now use Daikon.badOptionError. DtraceDiff and SampleTester passed "h:" to getopt, so -h required an argument. SampleTester also did not register --help. The badOptionError hint now suggests -h, which every tool supports. --- java/daikon/Daikon.java | 4 +--- java/daikon/test/BadOptionTest.java | 2 +- java/daikon/test/SampleTester.java | 6 +++--- java/daikon/test/split/SplitterFactoryTestUpdater.java | 2 +- java/daikon/tools/DtraceDiff.java | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index ebfc12022b..5ed127e929 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1520,9 +1520,7 @@ public static UserError badOptionError(Getopt g, String[] args) { return new UserError( "Bad command-line option" + (badOption == null ? "" : " " + badOption) - + "; run with --" - + help_SWITCH - + " for usage"); + + "; run with -h for usage"); } /** diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 66eb9f553e..8fb377c253 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -20,7 +20,7 @@ public class BadOptionTest { private static void assertBadOption(String badOption, ThrowingRunnable mainCall) { Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); assertEquals( - "Bad command-line option " + badOption + "; run with --help for usage", + "Bad command-line option " + badOption + "; run with -h for usage", String.valueOf(e.getMessage())); } diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index 40919edb6b..3666b0d0f8 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -83,13 +83,14 @@ public static void main(String[] args) throws IOException { LongOpt[] longopts = new LongOpt[] { + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.test.SampleTester", args, "h:", longopts); + Getopt g = new Getopt("daikon.test.SampleTester", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -128,8 +129,7 @@ public static void main(String[] args) throws IOException { throw new Daikon.NormalTermination(); case '?': - break; // getopt() already printed an error - + throw Daikon.badOptionError(g, args); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 0c806f6071..8f655d0ea6 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -261,7 +261,7 @@ private static String getTestClassText() { ps.println(" System.exit(1);"); ps.println(" break;"); ps.println(" case '?':"); - ps.println(" break;"); + ps.println(" throw Daikon.badOptionError(g, args);"); ps.println(" default:"); ps.println(" System.out.println(\"getopt() returned \" + c);"); ps.println(" break;"); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index c553441b79..41e858e614 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -124,7 +124,7 @@ public static void mainHelper(final String[] args) { new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.DtraceDiff", args, "h:", longopts); + Getopt g = new Getopt("daikon.tools.DtraceDiff", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { From 0c9f57527fff1460ac9e6703e937879d9a1407b4 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 5 Oct 2026 22:05:40 -0700 Subject: [PATCH 04/15] Describe bad command-line options precisely; register --help in all tools Add DaikonGetopt, which suppresses Getopt's own messages and reports whether an option is unrecognized, ambiguous, missing its argument, or given an unexpected argument. Register --help in DtraceDiff, UnionInvariants, ExtractConsequent, and InvariantChecker. Convert InstrumentHandler. Make SampleTester and SplitterFactoryTest exit cleanly on a bad option. Test DaikonGetopt directly rather than via Daikon.mainHelper, which resets global state. --- java/daikon/Daikon.java | 32 +---- java/daikon/DaikonGetopt.java | 114 +++++++++++++++++ java/daikon/MergeInvariants.java | 5 +- java/daikon/PrintInvariants.java | 5 +- java/daikon/UnionInvariants.java | 6 +- java/daikon/diff/Diff.java | 8 +- java/daikon/test/BadOptionTest.java | 117 +++++++++++++++--- java/daikon/test/SampleTester.java | 31 ++++- .../split/SplitterFactoryTestUpdater.java | 14 ++- java/daikon/tools/DtraceDiff.java | 6 +- java/daikon/tools/ExtractConsequent.java | 6 +- java/daikon/tools/InvariantChecker.java | 6 +- java/daikon/tools/compare/LogicalCompare.java | 5 +- java/daikon/tools/jtb/Annotate.java | 4 +- java/daikon/tools/jtb/CreateSpinfo.java | 4 +- .../runtimechecker/InstrumentHandler.java | 14 ++- 16 files changed, 292 insertions(+), 85 deletions(-) create mode 100644 java/daikon/DaikonGetopt.java diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 5ed127e929..402b78bfef 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1055,7 +1055,7 @@ static FileOptions read_options(String[] args, String usage) { new LongOpt(disc_reason_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(mem_stat_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.Daikon", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.Daikon", args, "ho:", longopts); int c; while ((c = g.getopt()) != -1) { @@ -1392,7 +1392,7 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { break; // case '?': - throw badOptionError(g, args); + throw g.badOptionError(); // default: throw new Daikon.BugInDaikon("getopt() returned " + c); @@ -1495,34 +1495,6 @@ public static String getOptarg(Getopt g) { return result; } - /** - * Returns an exception to throw when {@code g.getopt()} returns {@code '?'}, which indicates an - * unrecognized, ambiguous, or malformed command-line option. Getopt has already printed a message - * describing the problem. - * - * @param g a command-line argument processor whose {@code getopt()} just returned {@code '?'} - * @param args the command-line arguments that {@code g} is processing - * @return an exception that names the bad command-line argument - */ - public static UserError badOptionError(Getopt g, String[] args) { - int optopt = g.getOptopt(); - int optind = g.getOptind(); - String badOption; - if (optopt != 0) { - // A short option. It might be in the middle of an argument such as "-hx", so use optopt. - badOption = "-" + (char) optopt; - } else if (0 < optind && optind <= args.length) { - // A long option. Getopt has already advanced optind past it. - badOption = args[optind - 1]; - } else { - badOption = null; - } - return new UserError( - "Bad command-line option" - + (badOption == null ? "" : " " + badOption) - + "; run with -h for usage"); - } - /** * Invariants passed on the command line with the {@code --user_defined_invariant} option. A list * of class names in the format required by {@link Class#forName(String)}. diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java new file mode 100644 index 0000000000..c7776f5c4d --- /dev/null +++ b/java/daikon/DaikonGetopt.java @@ -0,0 +1,114 @@ +package daikon; + +import gnu.getopt.Getopt; +import gnu.getopt.LongOpt; + +/** + * A command-line option processor that does not print a message about a bad command-line option. + * Instead, when {@link #getopt()} returns {@code '?'}, a client should {@code throw + * g.badOptionError()}, which describes the problem. + */ +public class DaikonGetopt extends Getopt { + + /** + * Creates a command-line option processor that recognizes only short options. + * + * @param progname the name of the program, for use in messages + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt} + */ + public DaikonGetopt(String progname, String[] argv, String optstring) { + super(progname, argv, optstring); + opterr = false; + } + + /** + * Creates a command-line option processor that recognizes short and long options. + * + * @param progname the name of the program, for use in messages + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt} + * @param longopts the long options + */ + public DaikonGetopt(String progname, String[] argv, String optstring, LongOpt[] longopts) { + super(progname, argv, optstring, longopts); + opterr = false; + } + + /** + * Returns an exception to throw when {@link #getopt()} returns {@code '?'}, which indicates an + * unrecognized, ambiguous, or malformed command-line option. + * + * @return an exception that describes the bad command-line option + */ + public Daikon.UserError badOptionError() { + return new Daikon.UserError(badOptionMessage() + "; run with -h for usage"); + } + + /** + * Returns a description of the bad command-line option that {@link #getopt()} just rejected. + * + * @return a description of the bad command-line option + */ + private String badOptionMessage() { + // For every bad long option, Getopt clears nextchar and advances optind past the option. A + // bad short option has no such guarantee, because it may be followed by other short options in + // the same argument, as in "-xh". + if (long_options != null + && "".equals(nextchar) + && 0 < optind + && optind <= argv.length + && argv[optind - 1].startsWith("--")) { + return longOptionMessage(argv[optind - 1], long_options); + } + char c = (char) optopt; + if (c == ':' || optstring.indexOf(c) == -1) { + return "Unrecognized command-line option -" + c; + } else { + return "Command-line option -" + c + " requires an argument"; + } + } + + /** + * Returns a description of a bad long command-line option. + * + * @param arg the bad command-line argument, which starts with "--" + * @param longopts the long options that this processor recognizes + * @return a description of the bad command-line option + */ + private static String longOptionMessage(String arg, LongOpt[] longopts) { + int equalsPos = arg.indexOf('='); + String name = (equalsPos == -1) ? arg.substring(2) : arg.substring(2, equalsPos); + + // This mimics how Getopt matches a long option: an exact match takes precedence, and + // otherwise the name may be an unambiguous prefix of a long option. + LongOpt match = null; + boolean ambiguous = false; + for (LongOpt longopt : longopts) { + String longoptName = longopt.getName(); + if (longoptName.equals(name)) { + match = longopt; + ambiguous = false; + break; + } else if (longoptName.startsWith(name)) { + if (match == null) { + match = longopt; + } else { + ambiguous = true; + } + } + } + + if (ambiguous) { + return "Ambiguous command-line option --" + name; + } else if (match == null) { + return "Unrecognized command-line option --" + name; + } else if (equalsPos != -1 && match.getHasArg() == LongOpt.NO_ARGUMENT) { + return "Command-line option --" + match.getName() + " does not take an argument"; + } else if (equalsPos == -1 && match.getHasArg() == LongOpt.REQUIRED_ARGUMENT) { + return "Command-line option --" + match.getName() + " requires an argument"; + } else { + return "Bad command-line option " + arg; + } + } +} diff --git a/java/daikon/MergeInvariants.java b/java/daikon/MergeInvariants.java index a0fbc9f777..233e21461b 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -5,7 +5,6 @@ import daikon.split.PptSplitter; import daikon.suppress.NIS; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.FileNotFoundException; @@ -104,7 +103,7 @@ public static void mainHelper(String[] args) new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.MergeInvariants", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.MergeInvariants", args, "ho:", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -161,7 +160,7 @@ public static void mainHelper(String[] args) break; case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index 0f6786fbf1..c30a416ac2 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -24,7 +24,6 @@ import daikon.split.PptSplitter; import daikon.suppress.NIS; import daikon.suppress.NISuppressionSet; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.BufferedWriter; import java.io.File; @@ -344,7 +343,7 @@ public static void mainHelper(String[] args) new LongOpt( PrintInvariants.print_csharp_metadata_SWITCH, LongOpt.OPTIONAL_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.PrintInvariants", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.PrintInvariants", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -437,7 +436,7 @@ public static void mainHelper(String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index 7b32d6874d..29ab268efc 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -1,6 +1,5 @@ package daikon; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import org.plumelib.util.FilesPlume; @@ -50,9 +49,10 @@ public static void mainHelper(String[] args) throws Exception { LongOpt[] longopts = new LongOpt[] { + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.UnionInvariants", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.UnionInvariants", args, "ho:", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -91,7 +91,7 @@ public static void mainHelper(String[] args) throws Exception { break; // case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index ecc2406233..60f00f7e6b 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -4,13 +4,13 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Ppt; import daikon.PptConditional; import daikon.PptMap; import daikon.PptTopLevel; import daikon.inv.Invariant; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.FileNotFoundException; @@ -201,8 +201,8 @@ public static void mainHelper(final String[] args) new LongOpt(IGNORE_NUMBERED_EXITS_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - Getopt g = - new Getopt( + DaikonGetopt g = + new DaikonGetopt( "daikon.diff.Diff", args, "Hhyduastmxno:jzpevl", longOpts); int c; @@ -319,7 +319,7 @@ public static void mainHelper(final String[] args) logging = true; break; case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 8fb377c253..32f0fa485a 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -4,48 +4,127 @@ import static org.junit.Assert.assertThrows; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.PrintInvariants; +import daikon.UnionInvariants; +import daikon.tools.DtraceDiff; +import gnu.getopt.LongOpt; import org.junit.Test; import org.junit.function.ThrowingRunnable; -/** Tests that Daikon tools reject bad command-line options with a UserError. */ +/** + * Tests that Daikon tools reject bad command-line options with a UserError. + * + *

These tests do not call {@code Daikon.mainHelper}, because it resets Daikon's global state, + * which other tests in the same JVM depend on. + */ public class BadOptionTest { + /** The long options used by {@link #parse}. */ + private static final LongOpt[] longopts = + new LongOpt[] { + new LongOpt("help", LongOpt.NO_ARGUMENT, null, 0), + new LongOpt("config", LongOpt.REQUIRED_ARGUMENT, null, 0), + new LongOpt("config_option", LongOpt.REQUIRED_ARGUMENT, null, 0), + new LongOpt("debug", LongOpt.REQUIRED_ARGUMENT, null, 'd'), + new LongOpt("verbose", LongOpt.NO_ARGUMENT, null, 'v'), + }; + /** - * Asserts that running {@code mainCall} throws a UserError that names the given option. + * Parses the given command-line arguments, which must contain a bad option, and returns the + * resulting error message. * - * @param badOption the bad option, which the error message should name - * @param mainCall a call to a tool's {@code mainHelper} method + * @param args command-line arguments + * @return the message of the UserError for the bad option */ - private static void assertBadOption(String badOption, ThrowingRunnable mainCall) { - Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); - assertEquals( - "Bad command-line option " + badOption + "; run with -h for usage", - String.valueOf(e.getMessage())); + private static String parse(String... args) { + DaikonGetopt g = new DaikonGetopt("BadOptionTest", args, "ho:", longopts); + int c; + while ((c = g.getopt()) != -1) { + if (c == '?') { + return String.valueOf(g.badOptionError().getMessage()); + } + } + throw new AssertionError("no bad option"); + } + + /** + * Returns the expected message for a bad option. + * + * @param description a description of the bad option + * @return the expected message for a bad option + */ + private static String expected(String description) { + return description + "; run with -h for usage"; } /** Tests an unrecognized long option. */ @Test - public void testDaikonUnrecognizedLongOption() { - assertBadOption("--bogus", () -> Daikon.mainHelper(new String[] {"--bogus"})); + public void testUnrecognizedLongOption() { + assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus")); + assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus=3")); + assertEquals( + expected("Unrecognized command-line option --bogus"), parse("-h", "--bogus", "file")); + } + + /** Tests an unrecognized short option, alone and combined with a valid one. */ + @Test + public void testUnrecognizedShortOption() { + assertEquals(expected("Unrecognized command-line option -x"), parse("-x")); + assertEquals(expected("Unrecognized command-line option -x"), parse("-xh")); + assertEquals(expected("Unrecognized command-line option -x"), parse("-hx")); + assertEquals(expected("Unrecognized command-line option -:"), parse("-:")); + } + + /** Tests an ambiguous abbreviation of a long option. */ + @Test + public void testAmbiguousLongOption() { + assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf=x")); } - /** Tests an unrecognized short option that is combined with a valid one. */ + /** Tests options that are missing their required argument. */ @Test - public void testDaikonUnrecognizedShortOption() { - assertBadOption("-x", () -> Daikon.mainHelper(new String[] {"-xh"})); + public void testMissingArgument() { + assertEquals(expected("Command-line option --config requires an argument"), parse("--config")); + assertEquals(expected("Command-line option --debug requires an argument"), parse("--deb")); + assertEquals(expected("Command-line option -o requires an argument"), parse("-o")); + assertEquals(expected("Command-line option -o requires an argument"), parse("-ho")); } - /** Tests a long option that is missing its required argument. */ + /** Tests long options that are given an argument they do not take. */ @Test - public void testDaikonMissingArgument() { - assertBadOption("--config", () -> Daikon.mainHelper(new String[] {"--config"})); + public void testUnexpectedArgument() { + assertEquals( + expected("Command-line option --help does not take an argument"), parse("--help=x")); + assertEquals( + expected("Command-line option --verbose does not take an argument"), parse("--verb=x")); + } + + /** + * Asserts that running {@code mainCall} throws a UserError with the given message. + * + * @param description a description of the bad option + * @param mainCall a call to a tool's {@code mainHelper} method + */ + private static void assertBadOption(String description, ThrowingRunnable mainCall) { + Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); + assertEquals(expected(description), String.valueOf(e.getMessage())); } - /** Tests an unrecognized long option in a tool other than Daikon. */ + /** Tests an unrecognized long option in a tool. */ @Test public void testPrintInvariantsUnrecognizedOption() { assertBadOption( - "--bogus", () -> PrintInvariants.mainHelper(new String[] {"--bogus", "foo.inv.gz"})); + "Unrecognized command-line option --bogus", + () -> PrintInvariants.mainHelper(new String[] {"--bogus", "foo.inv.gz"})); + } + + /** Tests that tools accept the {@code --help} option that their usage messages document. */ + @Test + public void testHelpOption() { + assertThrows( + Daikon.NormalTermination.class, () -> DtraceDiff.mainHelper(new String[] {"--help"})); + assertThrows( + Daikon.NormalTermination.class, () -> UnionInvariants.mainHelper(new String[] {"--help"})); } } diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index 3666b0d0f8..6cfdb24b4d 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -8,6 +8,7 @@ import static org.junit.Assert.fail; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.Debug; import daikon.FileIO; import daikon.Global; @@ -18,7 +19,6 @@ import daikon.ValueTuple; import daikon.VarInfo; import daikon.inv.Invariant; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.IOException; @@ -67,7 +67,7 @@ public class SampleTester { /** The usage message for this program. */ private static String usage = StringsPlume.joinLines( - "Usage: java daikon.PrintInvariants [OPTION]... FILE", + "Usage: java daikon.test.SampleTester [OPTION]...", " -h, --" + Daikon.help_SWITCH, " Display this usage message", " --" + Daikon.config_option_SWITCH, @@ -78,8 +78,29 @@ public class SampleTester { " Specify a class, varinfos, and ppt to debug track.", " Format is class@ppt"); - @SuppressWarnings("JUnitMethodInvoked") // main() runs the tests without JUnit + /** + * Runs the tests without JUnit. + * + * @param args command-line arguments, like those of {@link #mainHelper} + * @throws IOException if there is a problem reading a test file + */ public static void main(String[] args) throws IOException { + 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 a test file + */ + @SuppressWarnings("JUnitMethodInvoked") // mainHelper() runs the tests without JUnit + public static void mainHelper(String[] args) throws IOException { LongOpt[] longopts = new LongOpt[] { @@ -90,7 +111,7 @@ public static void main(String[] args) throws IOException { new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.test.SampleTester", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.test.SampleTester", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -129,7 +150,7 @@ public static void main(String[] args) throws IOException { throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 8f655d0ea6..3326898d92 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -248,8 +248,16 @@ private static String getTestClassText() { ps.println(" \" -h Display this usage message\");"); ps.println(); ps.println(" public static void main(String[] args) {"); - ps.println( - " Getopt g = new Getopt(\"daikon.test.split.SplitterFactoryTest\", args, \"hs\");"); + ps.println(" try {"); + ps.println(" mainHelper(args);"); + ps.println(" } catch (Daikon.DaikonTerminationException e) {"); + ps.println(" Daikon.handleDaikonTerminationException(e);"); + ps.println(" }"); + ps.println(" }"); + ps.println(); + ps.println(" public static void mainHelper(String[] args) {"); + ps.println(" DaikonGetopt g ="); + ps.println(" new DaikonGetopt(\"daikon.test.split.SplitterFactoryTest\", args, \"hs\");"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); ps.println(" switch (c) {"); @@ -261,7 +269,7 @@ private static String getTestClassText() { ps.println(" System.exit(1);"); ps.println(" break;"); ps.println(" case '?':"); - ps.println(" throw Daikon.badOptionError(g, args);"); + ps.println(" throw g.badOptionError();"); ps.println(" default:"); ps.println(" System.out.println(\"getopt() returned \" + c);"); ps.println(" break;"); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 41e858e614..673a9b75f1 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -6,6 +6,7 @@ import static daikon.tools.nullness.NullnessUtil.*; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; @@ -114,6 +115,7 @@ public static void mainHelper(final String[] args) { LongOpt[] longopts = new LongOpt[] { + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), // Process only part of the trace file new LongOpt(Daikon.ppt_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.ppt_omit_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -124,7 +126,7 @@ public static void mainHelper(final String[] args) { new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.DtraceDiff", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.DtraceDiff", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -233,7 +235,7 @@ public static void mainHelper(final String[] args) { throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index c735960549..2146dd0296 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -5,6 +5,7 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.Ppt; @@ -113,12 +114,13 @@ public static void mainHelper(final String[] args) throws IOException { daikon.LogHelper.setupLogs(INFO); LongOpt[] longopts = new LongOpt[] { + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.ExtractConsequent", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.ExtractConsequent", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -146,7 +148,7 @@ public static void mainHelper(final String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index 734efb2d4b..40b8113b89 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -4,6 +4,7 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.Debug; import daikon.FileIO; import daikon.Global; @@ -127,6 +128,7 @@ public static void mainHelper(final String[] args) LongOpt[] longopts = new LongOpt[] { + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(output_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(dir_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -138,7 +140,7 @@ public static void mainHelper(final String[] args) new LongOpt(Daikon.ppt_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.InvariantChecker", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.InvariantChecker", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -186,7 +188,7 @@ public static void mainHelper(final String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index 0b63a306a4..36207510a6 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -5,6 +5,7 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; @@ -675,7 +676,7 @@ public static void mainHelper(final String[] args) extra_assumptions = new LinkedHashMap<>(); - Getopt g = new Getopt("daikon.tools.compare.LogicalCompare", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.compare.LogicalCompare", args, "h", longopts); int c; boolean user_filters = false; while ((c = g.getopt()) != -1) { @@ -734,7 +735,7 @@ public static void mainHelper(final String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); } } diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 6374408f88..2339c3b060 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -140,7 +140,7 @@ public static void mainHelper(final String[] args) throws Exception { new LongOpt(max_invariants_pp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(no_reflection_SWITCH, LongOpt.NO_ARGUMENT, null, 0) }; - Getopt g = new Getopt("daikon.tools.jtb.Annotate", args, "hs", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.Annotate", args, "hs", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -201,7 +201,7 @@ public static void mainHelper(final String[] args) throws Exception { slashslash = true; break; case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index 5dd0a86213..da3090c49a 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -88,7 +88,7 @@ public static void mainHelper(final String[] args) throws IOException { new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.jtb.CreateSpinfo", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.CreateSpinfo", args, "ho:", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -113,7 +113,7 @@ public static void mainHelper(final String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); case '?': - throw Daikon.badOptionError(g, args); + throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index ffd4e77a48..0b104ec3a0 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -5,11 +5,11 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; import daikon.tools.jtb.ParseResults; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.IOException; @@ -213,7 +213,8 @@ private Arguments readArguments(String[] args) { new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0) }; - Getopt g = new Getopt("daikon.tools.runtimechecker.InstrumentHandler", args, "hs", longopts); + DaikonGetopt g = + new DaikonGetopt("daikon.tools.runtimechecker.InstrumentHandler", args, "h", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -239,8 +240,15 @@ private Arguments readArguments(String[] args) { System.err.println("Unknown long option received: " + option_name); } break; + case 'h': + usageMessage(); + System.exit(0); + break; + case '?': + System.err.println(g.badOptionError().getMessage()); + return errorWhileReadingArguments; default: - System.out.println("unrecognized option" + c); + System.err.println("getopt() returned " + c); return errorWhileReadingArguments; } } From ec5ee500b09bf976fddf19a079fc90ad1f46b887 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 5 Oct 2026 22:30:41 -0700 Subject: [PATCH 05/15] DaikonGetopt.getopt() throws on a bad option; fix -h and --help handling - DaikonGetopt overrides getopt() to throw the UserError, so callers need no '?' case. - Use Getopt's longind instead of re-implementing long-option matching. - Remove the short-only constructor, which misparsed long options. - InstrumentHandler registers --help, does not call System.exit for -h, and omits the redundant usage hint. - Generated SplitterFactoryTest registers --help, throws NormalTermination for -h, and shows its own name in the usage message. - Describe the behavior change in the changelog. --- doc/CHANGELOG.md | 8 ++ java/daikon/Daikon.java | 3 - java/daikon/DaikonGetopt.java | 105 ++++++++++-------- java/daikon/MergeInvariants.java | 2 - java/daikon/PrintInvariants.java | 2 - java/daikon/UnionInvariants.java | 2 - java/daikon/diff/Diff.java | 2 - java/daikon/test/BadOptionTest.java | 24 ++-- java/daikon/test/SampleTester.java | 2 - .../split/SplitterFactoryTestUpdater.java | 18 +-- java/daikon/tools/DtraceDiff.java | 2 - java/daikon/tools/ExtractConsequent.java | 2 - java/daikon/tools/InvariantChecker.java | 2 - java/daikon/tools/compare/LogicalCompare.java | 2 - java/daikon/tools/jtb/Annotate.java | 2 - java/daikon/tools/jtb/CreateSpinfo.java | 2 - .../runtimechecker/InstrumentHandler.doc | 1 + .../runtimechecker/InstrumentHandler.java | 26 ++++- 18 files changed, 114 insertions(+), 93 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 6db6daed42..602599ef67 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -21,6 +21,14 @@ Further documentation can be found in: `make -C $DAIKONDIR/java javadoc` It is also available at . +## Version 5.9.1 (???, 2026) + +* Daikon and its tools now reject an unrecognized, ambiguous, or malformed + command-line option: they describe the problem and exit with a non-zero + status. Previously, Daikon printed its usage message and exited with + status 0, and most other tools ignored the bad option and continued. +* All tools accept `--help` as a synonym for `-h`. + ## Version 5.9.0 (September 1, 2026) * All Daikon tools now work with Java 8-26. diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 402b78bfef..2ae93904eb 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1391,9 +1391,6 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { } break; // - case '?': - throw g.badOptionError(); - // default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index c7776f5c4d..eb18ae5d1d 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -2,25 +2,16 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; +import org.checkerframework.checker.nullness.qual.Nullable; /** - * A command-line option processor that does not print a message about a bad command-line option. - * Instead, when {@link #getopt()} returns {@code '?'}, a client should {@code throw - * g.badOptionError()}, which describes the problem. + * A command-line option processor that throws a {@link Daikon.UserError} that describes a bad + * command-line option, instead of printing a message and returning {@code '?'}. */ public class DaikonGetopt extends Getopt { - /** - * Creates a command-line option processor that recognizes only short options. - * - * @param progname the name of the program, for use in messages - * @param argv the command-line arguments - * @param optstring the short options, in the format of {@link Getopt} - */ - public DaikonGetopt(String progname, String[] argv, String optstring) { - super(progname, argv, optstring); - opterr = false; - } + /** Text appended to the description of a bad command-line option, or null to append nothing. */ + private @Nullable String usageHint = "run with -h for usage"; /** * Creates a command-line option processor that recognizes short and long options. @@ -36,17 +27,48 @@ public DaikonGetopt(String progname, String[] argv, String optstring, LongOpt[] } /** - * Returns an exception to throw when {@link #getopt()} returns {@code '?'}, which indicates an - * unrecognized, ambiguous, or malformed command-line option. + * Sets the text that is appended to the description of a bad command-line option. The default + * tells the user to run the program with -h. + * + * @param usageHint text appended to the description of a bad command-line option, or null to + * append nothing + */ + public void setUsageHint(@Nullable String usageHint) { + this.usageHint = usageHint; + } + + /** + * Like {@link Getopt#getopt()}, but never returns {@code '?'}. + * + * @return the next option, as described in {@link Getopt#getopt()} + * @throws Daikon.UserError if the next option is unrecognized, ambiguous, or malformed + */ + @Override + public int getopt() { + int c = super.getopt(); + if (c == '?') { + throw badOptionError(); + } + return c; + } + + /** + * Returns an exception that describes the bad command-line option that {@link Getopt#getopt()} + * just rejected. * * @return an exception that describes the bad command-line option */ - public Daikon.UserError badOptionError() { - return new Daikon.UserError(badOptionMessage() + "; run with -h for usage"); + private Daikon.UserError badOptionError() { + String message = badOptionMessage(); + if (usageHint != null) { + message += "; " + usageHint; + } + return new Daikon.UserError(message); } /** - * Returns a description of the bad command-line option that {@link #getopt()} just rejected. + * Returns a description of the bad command-line option that {@link Getopt#getopt()} just + * rejected. * * @return a description of the bad command-line option */ @@ -54,12 +76,11 @@ private String badOptionMessage() { // For every bad long option, Getopt clears nextchar and advances optind past the option. A // bad short option has no such guarantee, because it may be followed by other short options in // the same argument, as in "-xh". - if (long_options != null - && "".equals(nextchar) + if ("".equals(nextchar) && 0 < optind && optind <= argv.length && argv[optind - 1].startsWith("--")) { - return longOptionMessage(argv[optind - 1], long_options); + return longOptionMessage(argv[optind - 1]); } char c = (char) optopt; if (c == ':' || optstring.indexOf(c) == -1) { @@ -70,40 +91,30 @@ private String badOptionMessage() { } /** - * Returns a description of a bad long command-line option. + * Returns a description of a bad long command-line option. Uses {@code longind}, which Getopt + * sets to the index of the long option that matched the argument, or to -1 if none matched. * * @param arg the bad command-line argument, which starts with "--" - * @param longopts the long options that this processor recognizes * @return a description of the bad command-line option */ - private static String longOptionMessage(String arg, LongOpt[] longopts) { + private String longOptionMessage(String arg) { int equalsPos = arg.indexOf('='); String name = (equalsPos == -1) ? arg.substring(2) : arg.substring(2, equalsPos); - - // This mimics how Getopt matches a long option: an exact match takes precedence, and - // otherwise the name may be an unambiguous prefix of a long option. - LongOpt match = null; - boolean ambiguous = false; - for (LongOpt longopt : longopts) { - String longoptName = longopt.getName(); - if (longoptName.equals(name)) { - match = longopt; - ambiguous = false; - break; - } else if (longoptName.startsWith(name)) { - if (match == null) { - match = longopt; - } else { - ambiguous = true; + LongOpt[] longopts = long_options; + if (longopts == null || longind == -1) { + return "Unrecognized command-line option --" + name; + } + LongOpt match = longopts[longind]; + // For an inexact match, Getopt sets longind to the first long option that has the given + // prefix. The match is ambiguous if a later long option also has the prefix. + if (!match.getName().equals(name)) { + for (int i = longind + 1; i < longopts.length; i++) { + if (longopts[i].getName().startsWith(name)) { + return "Ambiguous command-line option --" + name; } } } - - if (ambiguous) { - return "Ambiguous command-line option --" + name; - } else if (match == null) { - return "Unrecognized command-line option --" + name; - } else if (equalsPos != -1 && match.getHasArg() == LongOpt.NO_ARGUMENT) { + if (equalsPos != -1 && match.getHasArg() == LongOpt.NO_ARGUMENT) { return "Command-line option --" + match.getName() + " does not take an argument"; } else if (equalsPos == -1 && match.getHasArg() == LongOpt.REQUIRED_ARGUMENT) { return "Command-line option --" + match.getName() + " requires an argument"; diff --git a/java/daikon/MergeInvariants.java b/java/daikon/MergeInvariants.java index 233e21461b..3640767b23 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -159,8 +159,6 @@ public static void mainHelper(String[] args) } break; - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index c30a416ac2..803c578322 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -435,8 +435,6 @@ public static void mainHelper(String[] args) case 'h': System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index 29ab268efc..055d139dce 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -90,8 +90,6 @@ public static void mainHelper(String[] args) throws Exception { } break; // - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index 60f00f7e6b..e2a32e2f3f 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -318,8 +318,6 @@ public static void mainHelper(final String[] args) case 'l': logging = true; break; - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 32f0fa485a..611969b02e 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -39,13 +39,13 @@ public class BadOptionTest { */ private static String parse(String... args) { DaikonGetopt g = new DaikonGetopt("BadOptionTest", args, "ho:", longopts); - int c; - while ((c = g.getopt()) != -1) { - if (c == '?') { - return String.valueOf(g.badOptionError().getMessage()); - } - } - throw new AssertionError("no bad option"); + Daikon.UserError e = + assertThrows( + Daikon.UserError.class, + () -> { + while (g.getopt() != -1) {} + }); + return String.valueOf(e.getMessage()); } /** @@ -80,6 +80,16 @@ public void testUnrecognizedShortOption() { @Test public void testAmbiguousLongOption() { assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf=x")); + assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf")); + } + + /** Tests that a usage hint of null appends nothing to the description of a bad option. */ + @Test + public void testNoUsageHint() { + DaikonGetopt g = new DaikonGetopt("BadOptionTest", new String[] {"--bogus"}, "h", longopts); + g.setUsageHint(null); + Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals("Unrecognized command-line option --bogus", String.valueOf(e.getMessage())); } /** Tests options that are missing their required argument. */ diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index 6cfdb24b4d..d2b706d685 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -149,8 +149,6 @@ public static void mainHelper(String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 3326898d92..01d8ec2046 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -242,10 +242,11 @@ private static String getTestClassText() { ps.println(); ps.println(" private static String usage ="); ps.println(" StringsPlume.joinLines("); - ps.println(" \"Usage: java daikon.tools.CreateSpinfo FILE.java ...\","); + ps.println(" \"Usage: java daikon.test.split.SplitterFactoryTest [OPTION]...\","); ps.println( - " \" -s Save (do not delete) the splitter java files in the temp directory\","); - ps.println(" \" -h Display this usage message\");"); + " \" -s Save (do not delete) the splitter java files in the temp" + + " directory\","); + ps.println(" \" -h, --help Display this usage message\");"); ps.println(); ps.println(" public static void main(String[] args) {"); ps.println(" try {"); @@ -256,8 +257,12 @@ private static String getTestClassText() { ps.println(" }"); ps.println(); ps.println(" public static void mainHelper(String[] args) {"); + ps.println(" LongOpt[] longopts ="); + ps.println(" new LongOpt[] {new LongOpt(\"help\", LongOpt.NO_ARGUMENT, null, 'h')};"); ps.println(" DaikonGetopt g ="); - ps.println(" new DaikonGetopt(\"daikon.test.split.SplitterFactoryTest\", args, \"hs\");"); + ps.println( + " new DaikonGetopt(\"daikon.test.split.SplitterFactoryTest\", args, \"hs\"," + + " longopts);"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); ps.println(" switch (c) {"); @@ -266,10 +271,7 @@ private static String getTestClassText() { ps.println(" break;"); ps.println(" case 'h':"); ps.println(" System.out.println(usage);"); - ps.println(" System.exit(1);"); - ps.println(" break;"); - ps.println(" case '?':"); - ps.println(" throw g.badOptionError();"); + ps.println(" throw new Daikon.NormalTermination();"); ps.println(" default:"); ps.println(" System.out.println(\"getopt() returned \" + c);"); ps.println(" break;"); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 673a9b75f1..052aa98cdf 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -234,8 +234,6 @@ public static void mainHelper(final String[] args) { System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index 2146dd0296..26e2b033a2 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -147,8 +147,6 @@ public static void mainHelper(final String[] args) throws IOException { case 'h': System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index 40b8113b89..f0d3971c43 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -187,8 +187,6 @@ public static void mainHelper(final String[] args) case 'h': System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index 36207510a6..a748a2fa06 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -734,8 +734,6 @@ public static void mainHelper(final String[] args) case 'h': System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); } } diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 2339c3b060..15e9bba98b 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -200,8 +200,6 @@ public static void mainHelper(final String[] args) throws Exception { case 's': slashslash = true; break; - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index da3090c49a..8c88dd01eb 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -112,8 +112,6 @@ public static void mainHelper(final String[] args) throws IOException { case 'h': System.out.println(usage); throw new Daikon.NormalTermination(); - case '?': - throw g.badOptionError(); default: System.out.println("getopt() returned " + c); break; diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.doc b/java/daikon/tools/runtimechecker/InstrumentHandler.doc index 84ddec8845..b4d7089ab5 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.doc +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.doc @@ -7,6 +7,7 @@ instrument [OPTIONS] INV_FILE JAVA_FILES INV_FILE is an invariant file (suffix .inv or .inv.gz) created by Daikon. JAVA_FILES is one or more Java files that will be instrumented. OPTIONS: + -h, --help Display this usage message. --make_all_fields_public Make all declared fields public. --directory=DIRNAME Output directory; default: instrumented-classes --output_only_high_conf_invariants diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index 0b104ec3a0..dc4cc30e3d 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -89,6 +89,9 @@ public boolean handle(String[] args) { if (arguments == errorWhileReadingArguments) { return false; } + if (arguments == helpRequested) { + return true; + } // Set up debug traces; note this comes after reading command line options. daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); @@ -201,10 +204,14 @@ private static class Arguments { private static Arguments errorWhileReadingArguments = new Arguments("error while reading arguments", new ArrayList()); + /** Returned by {@link #readArguments} when the user requests the usage message. */ + private static Arguments helpRequested = new Arguments("help requested", new ArrayList()); + private Arguments readArguments(String[] args) { LongOpt[] longopts = new LongOpt[] { + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h'), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(output_only_high_conf_invariants_SWITCH, LongOpt.NO_ARGUMENT, null, 0), @@ -215,8 +222,19 @@ private Arguments readArguments(String[] args) { }; DaikonGetopt g = new DaikonGetopt("daikon.tools.runtimechecker.InstrumentHandler", args, "h", longopts); + // The caller prints the usage message when this command fails. + g.setUsageHint(null); int c; - while ((c = g.getopt()) != -1) { + while (true) { + try { + c = g.getopt(); + } catch (Daikon.UserError e) { + System.err.println(e.getMessage()); + return errorWhileReadingArguments; + } + if (c == -1) { + break; + } switch (c) { case 0: // got a long option @@ -242,11 +260,7 @@ private Arguments readArguments(String[] args) { break; case 'h': usageMessage(); - System.exit(0); - break; - case '?': - System.err.println(g.badOptionError().getMessage()); - return errorWhileReadingArguments; + return helpRequested; default: System.err.println("getopt() returned " + c); return errorWhileReadingArguments; From 08b5515bdd8ac1656cff15fe7ebef2a08925a016 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 5 Oct 2026 22:47:39 -0700 Subject: [PATCH 06/15] Address code review: empty long-option name, unexpected getopt() results, help to stdout * DaikonGetopt: describe "--=foo" as unrecognized; remove always-true conditions. * Throw BugInDaikon from every option switch's default branch. * InstrumentHandler: fail on an unhandled long option; print -h usage to stdout. * Generated SplitterFactoryTest: main runs the tests, so -s takes effect. --- java/daikon/DaikonGetopt.java | 9 +++---- java/daikon/MergeInvariants.java | 3 +-- java/daikon/PrintInvariants.java | 3 +-- java/daikon/UnionInvariants.java | 3 +-- java/daikon/diff/Diff.java | 3 +-- java/daikon/test/BadOptionTest.java | 1 + java/daikon/test/SampleTester.java | 3 +-- .../split/SplitterFactoryTestUpdater.java | 24 ++++++++++++------- java/daikon/tools/DtraceDiff.java | 3 +-- java/daikon/tools/ExtractConsequent.java | 3 +-- java/daikon/tools/InvariantChecker.java | 3 +-- java/daikon/tools/compare/LogicalCompare.java | 2 ++ java/daikon/tools/jtb/Annotate.java | 3 +-- java/daikon/tools/jtb/CreateSpinfo.java | 3 +-- .../tools/runtimechecker/CommandHandler.java | 15 ++++++++++-- .../runtimechecker/InstrumentHandler.java | 3 ++- java/daikon/tools/runtimechecker/Main.java | 15 ++++++++---- 17 files changed, 59 insertions(+), 40 deletions(-) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index eb18ae5d1d..dd19d15ceb 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -76,10 +76,7 @@ private String badOptionMessage() { // For every bad long option, Getopt clears nextchar and advances optind past the option. A // bad short option has no such guarantee, because it may be followed by other short options in // the same argument, as in "-xh". - if ("".equals(nextchar) - && 0 < optind - && optind <= argv.length - && argv[optind - 1].startsWith("--")) { + if ("".equals(nextchar) && argv[optind - 1].startsWith("--")) { return longOptionMessage(argv[optind - 1]); } char c = (char) optopt; @@ -101,6 +98,10 @@ private String longOptionMessage(String arg) { int equalsPos = arg.indexOf('='); String name = (equalsPos == -1) ? arg.substring(2) : arg.substring(2, equalsPos); LongOpt[] longopts = long_options; + // Getopt treats an empty name, as in "--=foo", as a prefix of every long option. + if (name.isEmpty()) { + return "Unrecognized command-line option " + arg; + } if (longopts == null || longind == -1) { return "Unrecognized command-line option --" + name; } diff --git a/java/daikon/MergeInvariants.java b/java/daikon/MergeInvariants.java index 3640767b23..d3fa8c82ce 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -160,8 +160,7 @@ public static void mainHelper(String[] args) break; default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index 803c578322..90513bea43 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -436,8 +436,7 @@ public static void mainHelper(String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index 055d139dce..ffe4ac855e 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -91,8 +91,7 @@ public static void mainHelper(String[] args) throws Exception { break; // default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index e2a32e2f3f..a7525b9571 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -319,8 +319,7 @@ public static void mainHelper(final String[] args) logging = true; break; default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 611969b02e..2805fb4a14 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -63,6 +63,7 @@ private static String expected(String description) { public void testUnrecognizedLongOption() { assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus")); assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus=3")); + assertEquals(expected("Unrecognized command-line option --=3"), parse("--=3")); assertEquals( expected("Unrecognized command-line option --bogus"), parse("-h", "--bogus", "file")); } diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index d2b706d685..a7ea2f079d 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -150,8 +150,7 @@ public static void mainHelper(String[] args) throws IOException { throw new Daikon.NormalTermination(); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 01d8ec2046..8711ac663f 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -195,7 +195,11 @@ private static String getTestClassText() { ps.println("import java.io.*;"); ps.println("import java.util.*;"); ps.println("import junit.framework.*;"); + ps.println("import org.junit.BeforeClass;"); ps.println("import org.junit.Test;"); + ps.println("import org.junit.runner.JUnitCore;"); + ps.println("import org.junit.runner.Result;"); + ps.println("import org.junit.runner.notification.Failure;"); ps.println("import org.plumelib.util.FilesPlume;"); ps.println("import org.plumelib.util.StringsPlume;"); ps.println("import org.plumelib.util.UtilPlume;"); @@ -229,11 +233,6 @@ private static String getTestClassText() { ps.println(" // java files it produces, changing the order that the setUpTests"); ps.println(" // commands are run will cause the tests to fail."); ps.println(); - ps.println(" /** Do not instantiate. */"); - ps.println(" private SplitterFactoryTest() {"); - ps.println(" throw new Error(\"Do not instantiate\");"); - ps.println(" }"); - ps.println(); ps.println(" private static String targetDir = \"" + targetDir + "\";"); ps.println(); ps.println(" private static @Nullable String tempDir = null;"); @@ -273,10 +272,16 @@ private static String getTestClassText() { ps.println(" System.out.println(usage);"); ps.println(" throw new Daikon.NormalTermination();"); ps.println(" default:"); - ps.println(" System.out.println(\"getopt() returned \" + c);"); - ps.println(" break;"); + ps.println(" throw new Daikon.BugInDaikon(\"getopt() returned \" + c);"); ps.println(" }"); ps.println(" }"); + ps.println(" Result result = JUnitCore.runClasses(SplitterFactoryTest.class);"); + ps.println(" for (Failure failure : result.getFailures()) {"); + ps.println(" System.out.println(failure);"); + ps.println(" }"); + ps.println(" if (!result.wasSuccessful()) {"); + ps.println(" throw new Daikon.UserError(result.getFailureCount() + \" tests failed\");"); + ps.println(" }"); ps.println(" }"); ps.println(); @@ -330,7 +335,8 @@ private static String getTestClassText() { * SplitterFactoryTest to set up the needed files to run the tests on. */ public static void appendSetUpTest(PrintStream ps) { - ps.println(" private static void setUpTests() {"); + ps.println(" @BeforeClass"); + ps.println(" public static void setUpTests() {"); ps.println(" List spinfoFiles;"); ps.println(" List declsFiles;"); for (int i = 0; i < spinfoFileLists.size(); i++) { @@ -360,7 +366,7 @@ private static void appendTests(PrintStream ps) { ps.println(); for (String className : classNames) { ps.println(" @Test"); - ps.println(" public static void test" + className + "() {"); + ps.println(" public void test" + className + "() {"); ps.println(" assertEqualFiles(\"" + className + ".java\");"); ps.println(" }"); ps.println(); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 052aa98cdf..864df768c1 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -235,8 +235,7 @@ public static void mainHelper(final String[] args) { throw new Daikon.NormalTermination(); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index 26e2b033a2..134680f061 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -148,8 +148,7 @@ public static void mainHelper(final String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } // The index of the first non-option argument -- the name of the file diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index f0d3971c43..a44487ad54 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -188,8 +188,7 @@ public static void mainHelper(final String[] args) System.out.println(usage); throw new Daikon.NormalTermination(); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index a748a2fa06..bbffbe6758 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -734,6 +734,8 @@ public static void mainHelper(final String[] args) case 'h': System.out.println(usage); throw new Daikon.NormalTermination(); + default: + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 15e9bba98b..a369cf722a 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -201,8 +201,7 @@ public static void mainHelper(final String[] args) throws Exception { slashslash = true; break; default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index 8c88dd01eb..0d9ff03898 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -113,8 +113,7 @@ public static void mainHelper(final String[] args) throws IOException { System.out.println(usage); throw new Daikon.NormalTermination(); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/runtimechecker/CommandHandler.java b/java/daikon/tools/runtimechecker/CommandHandler.java index 7097f6ac9d..d00e0f2a15 100644 --- a/java/daikon/tools/runtimechecker/CommandHandler.java +++ b/java/daikon/tools/runtimechecker/CommandHandler.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintStream; import java.io.UncheckedIOException; /** @@ -22,20 +23,30 @@ public boolean handle(String[] args) { throw new UnsupportedOperationException(); } + /** Prints the usage message to standard error. */ public void usageMessage() { + usageMessage(System.err); + } + + /** + * Prints the usage message. + * + * @param out where to print the usage message + */ + public void usageMessage(PrintStream out) { String[] classnameArray = getClass().getName().split("\\."); String simpleClassname = classnameArray[classnameArray.length - 1]; String docFile = simpleClassname + ".doc"; InputStream in = getClass().getResourceAsStream(docFile); if (in == null) { - System.err.println("Didn't find documentation for " + getClass()); + out.println("Didn't find documentation for " + getClass()); return; } try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { String line; while ((line = reader.readLine()) != null) { - System.err.println(line); + out.println(line); } } catch (IOException e) { throw new UncheckedIOException("problem reading " + docFile, e); diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index dc4cc30e3d..c8205103c9 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -256,10 +256,11 @@ private Arguments readArguments(String[] args) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { System.err.println("Unknown long option received: " + option_name); + return errorWhileReadingArguments; } break; case 'h': - usageMessage(); + usageMessage(System.out); return helpRequested; default: System.err.println("getopt() returned " + c); diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 9259e07f45..45a59c4eda 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,5 +1,6 @@ package daikon.tools.runtimechecker; +import java.io.PrintStream; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -10,9 +11,15 @@ */ public class Main extends CommandHandler { - protected void usageMessage(List handlers) { + /** + * Prints the usage message of each handler. + * + * @param handlers the handlers whose usage messages to print + * @param out where to print the usage messages + */ + protected void usageMessage(List handlers, PrintStream out) { for (CommandHandler h : handlers) { - h.usageMessage(); + h.usageMessage(out); } } @@ -34,8 +41,8 @@ public void nonStaticMain(String[] args) { System.exit(1); } if (args[0].toUpperCase(Locale.ENGLISH).equals("HELP") || args[0].equals("?")) { - usageMessage(); - usageMessage(handlers); + usageMessage(System.out); + usageMessage(handlers, System.out); System.exit(0); } From 91a6600d57001252b65b0c1c5a155bb386705083 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 08:26:12 -0700 Subject: [PATCH 07/15] Fix CI: accept Diff's -u as a no-op, drop unused -z; satisfy Error Prone in BadOptionTest --- doc/daikon.texinfo | 2 +- java/daikon/diff/Diff.java | 4 +++- java/daikon/test/BadOptionTest.java | 16 ++++++++++------ tests/diff-tests/schedule2/schedule2-u.txt.goal | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/doc/daikon.texinfo b/doc/daikon.texinfo index 2c7d2f72e6..52364ddd43 100644 --- a/doc/daikon.texinfo +++ b/doc/daikon.texinfo @@ -6750,7 +6750,7 @@ Display the tree of all invariants. Includes invariants that are the same in @var{file1} and @var{file2}, and unjustified invariants. @item -u -Include ``uninteresting'' invariants in the tree of differing invariants. +Has no effect; accepted for backward compatibility. @item -y @itemx --ignore_unjustified diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index a7525b9571..d70585a9ce 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -204,7 +204,7 @@ public static void mainHelper(final String[] args) DaikonGetopt g = new DaikonGetopt( "daikon.diff.Diff", args, - "Hhyduastmxno:jzpevl", longOpts); + "Hhyduastmxno:jpevl", longOpts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -269,6 +269,8 @@ public static void mainHelper(final String[] args) optionSelected = true; printDiff = true; break; + case 'u': // included for legacy code; has no effect + break; case 'a': optionSelected = true; printAll = true; diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 2805fb4a14..3cb2b2bbcb 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -39,15 +39,19 @@ public class BadOptionTest { */ private static String parse(String... args) { DaikonGetopt g = new DaikonGetopt("BadOptionTest", args, "ho:", longopts); - Daikon.UserError e = - assertThrows( - Daikon.UserError.class, - () -> { - while (g.getopt() != -1) {} - }); + Daikon.UserError e = assertThrows(Daikon.UserError.class, () -> consumeAll(g)); return String.valueOf(e.getMessage()); } + /** + * Calls {@code getopt()} until all command-line arguments have been processed. + * + * @param g the command-line parser + */ + private static void consumeAll(DaikonGetopt g) { + while (g.getopt() != -1) {} + } + /** * Returns the expected message for a bad option. * diff --git a/tests/diff-tests/schedule2/schedule2-u.txt.goal b/tests/diff-tests/schedule2/schedule2-u.txt.goal index ef71a1c0d9..b549ab6340 100644 --- a/tests/diff-tests/schedule2/schedule2-u.txt.goal +++ b/tests/diff-tests/schedule2/schedule2-u.txt.goal @@ -1,4 +1,3 @@ -getopt() returned 117 <::next_pid >= 0 {0-}, ::next_pid >= 0 {1+}> (Una,SUJ) From a37f57a69fdfa9eeb863977555be2a43eba6ce73 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 08:34:57 -0700 Subject: [PATCH 08/15] Remove Diff's no-op -u option --- doc/daikon.texinfo | 3 - java/daikon/diff/Diff.java | 4 +- tests/diff-tests/Makefile.common | 4 +- .../diff-tests/schedule2/schedule2-u.txt.goal | 76 ------------------- 4 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 tests/diff-tests/schedule2/schedule2-u.txt.goal diff --git a/doc/daikon.texinfo b/doc/daikon.texinfo index 52364ddd43..63824ae74e 100644 --- a/doc/daikon.texinfo +++ b/doc/daikon.texinfo @@ -6749,9 +6749,6 @@ invariants must be justified. Does not print ``uninteresting'' invariants Display the tree of all invariants. Includes invariants that are the same in @var{file1} and @var{file2}, and unjustified invariants. -@item -u -Has no effect; accepted for backward compatibility. - @item -y @itemx --ignore_unjustified Include (statistically) unjustified invariants. diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index d70585a9ce..343665266b 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -204,7 +204,7 @@ public static void mainHelper(final String[] args) DaikonGetopt g = new DaikonGetopt( "daikon.diff.Diff", args, - "Hhyduastmxno:jpevl", longOpts); + "Hhydastmxno:jpevl", longOpts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -269,8 +269,6 @@ public static void mainHelper(final String[] args) optionSelected = true; printDiff = true; break; - case 'u': // included for legacy code; has no effect - break; case 'a': optionSelected = true; printAll = true; diff --git a/tests/diff-tests/Makefile.common b/tests/diff-tests/Makefile.common index 7a22461dfd..014605b8b4 100644 --- a/tests/diff-tests/Makefile.common +++ b/tests/diff-tests/Makefile.common @@ -20,10 +20,10 @@ INV_FILES := $(PROGRAM)-1.inv.gz $(PROGRAM)-2.inv.gz # An output file must satisfy the pattern program%.txt, where the % is # the options passed to Diff ## Must reinstate -a and -ap and -p in V3 as soon as possible. -MDE 6/10/2003 -# OUTPUT := $(PROGRAM).txt $(PROGRAM)-u.txt $(PROGRAM)-a.txt $(PROGRAM)-p.txt \ +# OUTPUT := $(PROGRAM).txt $(PROGRAM)-a.txt $(PROGRAM)-p.txt \ # $(PROGRAM)-e.txt $(PROGRAM)-ap.txt $(PROGRAM)-t.txt \ # $(PROGRAM)-jt.txt -OUTPUT := $(PROGRAM).txt $(PROGRAM)-u.txt \ +OUTPUT := $(PROGRAM).txt \ $(PROGRAM)-e.txt $(PROGRAM)-t.txt \ $(PROGRAM)-jt.txt # Must be handled differently than OUTPUT, because map files are diff --git a/tests/diff-tests/schedule2/schedule2-u.txt.goal b/tests/diff-tests/schedule2/schedule2-u.txt.goal deleted file mode 100644 index b549ab6340..0000000000 --- a/tests/diff-tests/schedule2/schedule2-u.txt.goal +++ /dev/null @@ -1,76 +0,0 @@ - - <::next_pid >= 0 {0-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - <::next_pid >= 0 {0-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - - (return is a pointer) {1+}> (Nul,MJ) - (prio >= -1) {1+}> (Nul,MJ) - (prio >= 0) {1+}> (Nul,MJ) - (prio != 0) {0.9999+}> (Nul,MJ) - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - (Una,DJJ) - - <*prio >= -1 {1+}, *prio >= -2 {0-}> (Una,DJU) - <*command != 0 {1+}, null> (Una,JM) - - (*prio >= -1) {1+}> (Nul,MJ) - (orig(*prio) >= -1) {1+}> (Nul,MJ) - <(return == 1) ==> (*prio != 0) {1+}, null> (Nul,JM) - (*command == 0) {1+}> (Bin,MJ) - <(*prio == 0) ==> (orig(*prio) == 0) {1+}, null> (Bin,JM) - <*prio >= -1 {1+}, *prio >= -2 {0-}> (Una,DJU) - <*command != 0 {1+}, null> (Una,JM) - - <::next_pid >= 0 {0.4444-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - <::next_pid >= 0 {0.4444-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - = 0.0 {1+}, ratio >= -1.0 {0.0721-}> (Una,DJU) - - = 0.0 {1+}, ratio >= -1.0 {0.0721-}> (Una,DJU) - (Una,DJJ) - - (Una,JM) - - = orig(::next_pid) {1+}> (Bin,MJ) - <::next_pid - orig(::next_pid) - 1 == 0 {1+}, null> (Bin,JM) - (orig(::next_pid) == 0) {1+}> (Bin,MJ) - <::next_pid >= 1 {1+}, ::next_pid >= 0 {0.3333-}> (Una,DJU) - (Una,DUJ) - (Una,JM) - (Una,DJJ) - - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - - (return is a pointer) {1+}> (Nul,MJ) - (prio >= -1) {1+}> (Nul,MJ) - (prio >= 0) {1+}> (Nul,MJ) - (prio != 0) {0.9999+}> (Nul,MJ) - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - (Una,DJJ) - - = -1 {1+}, prio >= -2 {0-}> (Una,DJU) - (Una,JM) - (Una,JM) - - = -1 {1+}, prio >= -2 {0-}> (Una,DJU) - (Una,JM) - (Una,JM) - (Una,JM) - (Una,JM) - - (Una,SUJ) - - (Una,SUJ) - (Una,DJJ) - - <::next_pid >= 0 {1+}, ::next_pid >= 0 {0.7779-}> (Una,SJU) - (Una,SUJ) - (Una,JM) - - <::next_pid >= 0 {1+}, ::next_pid >= 0 {0.7779-}> (Una,SJU) - (Una,SUJ) - (Una,JM) - (Una,DJJ) From df9fb2140b7002e7aea29588de7a2b14fc90aac5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 09:09:42 -0700 Subject: [PATCH 09/15] Address code review: help handling in DaikonGetopt, all tools accept -h/--help * DaikonGetopt optionally takes a usage message and handles -h and --help; tools no longer duplicate that handling. It also treats ':' as a bad option. New DaikonGetopt.nonOptionArgs for tools that take only files. * Tools that parsed arguments by hand (ReadTrace, SplitDtrace, DtraceNonceFixer, CreateSpinfoC, TraceSelect, runtimechecker.Main, WriteViolationFile) accept -h/--help; AnnotateNullable and BuildJDK accept --help; Chicory and DynComp exit 0 for --help. * Unhandled long options are a BugInDaikon, not a user error. * InvariantChecker no longer registers the unimplemented --ppt-select-pattern. * Diff with no input files is a user error. * Generated SplitterFactoryTest prints failure traces and pluralizes. --- doc/CHANGELOG.md | 5 +- doc/daikon.texinfo | 3 + java/daikon/AnnotateNullable.java | 16 +++- java/daikon/Chicory.java | 2 +- java/daikon/Daikon.java | 23 +++--- java/daikon/DaikonGetopt.java | 75 ++++++++++++++++++- java/daikon/DynComp.java | 2 +- java/daikon/MergeInvariants.java | 14 +--- java/daikon/PrintInvariants.java | 13 +--- java/daikon/SplitDtrace.java | 28 +++++-- java/daikon/UnionInvariants.java | 13 +--- java/daikon/dcomp/BuildJDK.java | 4 + java/daikon/dcomp/BuildJDK24.java | 4 + java/daikon/diff/Diff.java | 30 ++++---- java/daikon/test/BadOptionTest.java | 38 ++++++++++ java/daikon/test/SampleTester.java | 15 +--- .../split/SplitterFactoryTestUpdater.java | 16 ++-- java/daikon/tools/DtraceDiff.java | 14 +--- java/daikon/tools/DtraceNonceFixer.java | 10 ++- java/daikon/tools/ExtractConsequent.java | 13 +--- java/daikon/tools/InvariantChecker.java | 14 +--- java/daikon/tools/ReadTrace.java | 29 ++++++- java/daikon/tools/TraceSelect.java | 17 ++++- java/daikon/tools/compare/LogicalCompare.java | 14 +--- java/daikon/tools/jtb/Annotate.java | 13 +--- java/daikon/tools/jtb/CreateSpinfo.java | 13 +--- java/daikon/tools/jtb/CreateSpinfoC.java | 41 +++++++--- .../tools/runtimechecker/CommandHandler.java | 3 +- .../runtimechecker/InstrumentHandler.java | 25 +++---- java/daikon/tools/runtimechecker/Main.java | 5 +- .../runtimechecker/WriteViolationFile.java | 5 ++ 31 files changed, 326 insertions(+), 191 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 602599ef67..9493cc5a69 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -27,7 +27,10 @@ Further documentation can be found in: command-line option: they describe the problem and exit with a non-zero status. Previously, Daikon printed its usage message and exited with status 0, and most other tools ignored the bad option and continued. -* All tools accept `--help` as a synonym for `-h`. +* 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. ## Version 5.9.0 (September 1, 2026) 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..957fe44e40 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.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/Daikon.java b/java/daikon/Daikon.java index 2ae93904eb..7a9952b75c 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1022,7 +1022,6 @@ static FileOptions read_options(String[] args, String usage) { LongOpt[] longopts = new LongOpt[] { // Control output - new LongOpt(help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(no_text_output_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(format_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(show_progress_SWITCH, LongOpt.NO_ARGUMENT, null, 0), @@ -1055,7 +1054,7 @@ static FileOptions read_options(String[] args, String usage) { new LongOpt(disc_reason_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(mem_stat_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.Daikon", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.Daikon", args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { @@ -1065,10 +1064,7 @@ static FileOptions read_options(String[] args, String usage) { String option_name = longopts[g.getLongind()].getName(); // Control output - if (help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (no_text_output_SWITCH.equals(option_name)) { + if (no_text_output_SWITCH.equals(option_name)) { no_text_output = true; } else if (format_SWITCH.equals(option_name)) { String format_name = getOptarg(g); @@ -1367,12 +1363,9 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { } else if (mem_stat_SWITCH.equals(option_name)) { use_mem_monitor = true; } else { - throw new Daikon.UserError("Unknown option " + option_name + " on command line"); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'o': String inv_filename = getOptarg(g); @@ -1492,6 +1485,16 @@ public static String getOptarg(Getopt g) { return result; } + /** + * Returns true if the command-line argument requests a usage message. + * + * @param arg a command-line argument + * @return true if {@code arg} is "-h" or "--help" + */ + public static boolean isHelpArg(String arg) { + return arg.equals("-h") || arg.equals("--" + help_SWITCH); + } + /** * Invariants passed on the command line with the {@code --user_defined_invariant} option. A list * of class names in the format required by {@link Class#forName(String)}. diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index dd19d15ceb..66ff21360d 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -2,14 +2,24 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; +import java.util.Arrays; import org.checkerframework.checker.nullness.qual.Nullable; /** * A command-line option processor that throws a {@link Daikon.UserError} that describes a bad - * command-line option, instead of printing a message and returning {@code '?'}. + * command-line option, instead of printing a message and returning {@code '?'} or {@code ':'}. + * + *

If constructed with a usage message, it also handles {@code -h} and {@code --help}: it prints + * the usage message and throws {@link Daikon.NormalTermination}. */ public class DaikonGetopt extends Getopt { + /** + * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles + * them. + */ + private final @Nullable String usage; + /** Text appended to the description of a bad command-line option, or null to append nothing. */ private @Nullable String usageHint = "run with -h for usage"; @@ -23,9 +33,60 @@ public class DaikonGetopt extends Getopt { */ public DaikonGetopt(String progname, String[] argv, String optstring, LongOpt[] longopts) { super(progname, argv, optstring, longopts); + this.usage = null; opterr = false; } + /** + * Creates a command-line option processor that recognizes short and long options, plus {@code -h} + * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. + * + * @param progname the name of the program, for use in messages + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} + * @param longopts the long options; must not contain {@code help} + * @param usage the usage message to print for {@code -h} and {@code --help} + */ + public DaikonGetopt( + String progname, String[] argv, String optstring, LongOpt[] longopts, String usage) { + super(progname, argv, optstring + "h", withHelp(longopts)); + this.usage = usage; + opterr = false; + } + + /** + * Returns the given long options plus {@code --help}. + * + * @param longopts long options + * @return {@code longopts} plus {@code --help} + */ + private static LongOpt[] withHelp(LongOpt[] longopts) { + LongOpt[] result = Arrays.copyOf(longopts, longopts.length + 1); + result[longopts.length] = new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h'); + return result; + } + + /** + * Processes the command-line arguments of a program that takes no options other than {@code -h} + * and {@code --help}. + * + * @param progname the name of the program, for use in messages + * @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 progname, String[] argv, String usage) { + DaikonGetopt g = new DaikonGetopt(progname, argv, "", 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(argv, g.getOptind(), argv.length); + } + /** * Sets the text that is appended to the description of a bad command-line option. The default * tells the user to run the program with -h. @@ -38,17 +99,25 @@ public void setUsageHint(@Nullable String usageHint) { } /** - * Like {@link Getopt#getopt()}, but never returns {@code '?'}. + * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns + * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was + * constructed with a usage message, also never returns {@code 'h'}. * * @return the next option, as described in {@link Getopt#getopt()} * @throws Daikon.UserError if the next option is unrecognized, ambiguous, or malformed + * @throws Daikon.NormalTermination after printing the usage message, if the next option is {@code + * -h} or {@code --help} and this was constructed with a usage message */ @Override public int getopt() { int c = super.getopt(); - if (c == '?') { + if (c == '?' || c == ':') { throw badOptionError(); } + if (c == 'h' && usage != null) { + System.out.println(usage); + throw new Daikon.NormalTermination(); + } return c; } 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/MergeInvariants.java b/java/daikon/MergeInvariants.java index d3fa8c82ce..84c79dae0b 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -96,14 +96,13 @@ public static void mainHelper(String[] args) LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.MergeInvariants", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.MergeInvariants", args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -111,10 +110,7 @@ public static void mainHelper(String[] args) // long option case 0: String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.config_option_SWITCH.equals(option_name)) { + if (Daikon.config_option_SWITCH.equals(option_name)) { String item = Daikon.getOptarg(g); daikon.config.Configuration.getInstance().apply(item); break; @@ -132,14 +128,10 @@ public static void mainHelper(String[] args) "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error); } } else { - throw new Daikon.UserError("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case 'o': String output_inv_filename = Daikon.getOptarg(g); diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index 90513bea43..b8df808759 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -328,7 +328,6 @@ public static void mainHelper(String[] args) LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.format_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.output_num_samples_SWITCH, LongOpt.NO_ARGUMENT, null, 0), @@ -343,17 +342,14 @@ public static void mainHelper(String[] args) new LongOpt( PrintInvariants.print_csharp_metadata_SWITCH, LongOpt.OPTIONAL_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.PrintInvariants", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.PrintInvariants", args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { + if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { if (ppt_regexp != null) { throw new Error( "multiple --" @@ -429,12 +425,9 @@ public static void mainHelper(String[] args) } else if (Daikon.wrap_xml_SWITCH.equals(option_name)) { wrap_xml = true; } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/SplitDtrace.java b/java/daikon/SplitDtrace.java index d13b4b81cb..ab93f582b3 100644 --- a/java/daikon/SplitDtrace.java +++ b/java/daikon/SplitDtrace.java @@ -29,6 +29,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 +43,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("daikon.SplitDtrace", 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; diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index ffe4ac855e..1dd0f5c891 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -49,28 +49,21 @@ public static void mainHelper(String[] args) throws Exception { LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.UnionInvariants", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.UnionInvariants", args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { + if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { Daikon.suppress_redundant_invariants_with_simplify = true; } else { - throw new Daikon.UserError("Unknown option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'o': String inv_filename = Daikon.getOptarg(g); 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/diff/Diff.java b/java/daikon/diff/Diff.java index 343665266b..e40e5ba988 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -68,13 +68,19 @@ public final class Diff { " For a list of flags, see the Daikon manual, which appears in the ", " Daikon distribution and also at http://plse.cs.washington.edu/daikon/."); - /** The long command line options. */ - private static final String HELP_SWITCH = "help"; - + /** The long command-line option that specifies the comparator to sort the first set of invs. */ private static final String INV_SORT_COMPARATOR1_SWITCH = "invSortComparator1"; + + /** The long command-line option that specifies the comparator to sort the second set of invs. */ private static final String INV_SORT_COMPARATOR2_SWITCH = "invSortComparator2"; + + /** The long command-line option that specifies the comparator to pair up invs. */ private static final String INV_PAIR_COMPARATOR_SWITCH = "invPairComparator"; + + /** The long command-line option that is a synonym for -y. */ private static final String IGNORE_UNJUSTIFIED_SWITCH = "ignore_unjustified"; + + /** The long command-line option that ignores numbered exit points. */ private static final String IGNORE_NUMBERED_EXITS_SWITCH = "ignore_exitNN"; /** Determine which ppts should be paired together in the tree. */ @@ -193,7 +199,6 @@ public static void mainHelper(final String[] args) LongOpt[] longOpts = new LongOpt[] { - new LongOpt(HELP_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(INV_SORT_COMPARATOR1_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(INV_SORT_COMPARATOR2_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(INV_PAIR_COMPARATOR_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -202,19 +207,14 @@ public static void mainHelper(final String[] args) }; DaikonGetopt g = - new DaikonGetopt( - "daikon.diff.Diff", args, - "Hhydastmxno:jpevl", longOpts); + new DaikonGetopt("daikon.diff.Diff", args, "Hydastmxno:jpevl", longOpts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String optionName = longOpts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(optionName)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (INV_SORT_COMPARATOR1_SWITCH.equals(optionName)) { + if (INV_SORT_COMPARATOR1_SWITCH.equals(optionName)) { if (invSortComparator1Classname != null) { throw new Error( "multiple --" @@ -252,12 +252,9 @@ public static void mainHelper(final String[] args) ignoreNumberedExits = true; break; } else { - throw new RuntimeException("Unknown long option received: " + optionName); + throw new Daikon.BugInDaikon("Unhandled long option " + optionName); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'H': PrintAllVisitor.HUMAN_OUTPUT = true; break; @@ -417,8 +414,7 @@ public static void mainHelper(final String[] args) v1.printAll(); return; } else { - System.out.println(usage); - throw new Daikon.NormalTermination(); + throw new Daikon.UserError("No invariant files specified" + Daikon.lineSep + usage); } if (logging) { diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 3cb2b2bbcb..41edce25be 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -1,11 +1,13 @@ package daikon.test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import daikon.Daikon; import daikon.DaikonGetopt; import daikon.PrintInvariants; +import daikon.SplitDtrace; import daikon.UnionInvariants; import daikon.tools.DtraceDiff; import gnu.getopt.LongOpt; @@ -106,6 +108,20 @@ public void testMissingArgument() { assertEquals(expected("Command-line option -o requires an argument"), parse("-ho")); } + /** Tests options that are missing their required argument, when the optstring starts with ':'. */ + @Test + public void testMissingArgumentLeadingColon() { + DaikonGetopt g = new DaikonGetopt("BadOptionTest", new String[] {"-o"}, ":ho:", longopts); + Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals( + expected("Command-line option -o requires an argument"), String.valueOf(e.getMessage())); + g = new DaikonGetopt("BadOptionTest", new String[] {"--config"}, ":ho:", longopts); + e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals( + expected("Command-line option --config requires an argument"), + String.valueOf(e.getMessage())); + } + /** Tests long options that are given an argument they do not take. */ @Test public void testUnexpectedArgument() { @@ -141,5 +157,27 @@ public void testHelpOption() { Daikon.NormalTermination.class, () -> DtraceDiff.mainHelper(new String[] {"--help"})); assertThrows( Daikon.NormalTermination.class, () -> UnionInvariants.mainHelper(new String[] {"--help"})); + assertThrows( + Daikon.NormalTermination.class, () -> SplitDtrace.mainHelper(new String[] {"-h", "x"})); + } + + /** Tests {@link DaikonGetopt#nonOptionArgs}. */ + @Test + public void testNonOptionArgs() { + assertArrayEquals( + new String[] {"a.dtrace", "b.dtrace"}, + DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"a.dtrace", "b.dtrace"}, "u")); + assertArrayEquals( + new String[] {"-a.dtrace"}, + DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"--", "-a.dtrace"}, "u")); + assertBadOption( + "Unrecognized command-line option --bogus", + () -> DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"x", "--bogus"}, "u")); + assertBadOption( + "Unrecognized command-line option -x", + () -> DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"-x"}, "u")); + assertThrows( + Daikon.NormalTermination.class, + () -> DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"x", "--help"}, "u")); } } diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index a7ea2f079d..17718e4840 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -104,14 +104,13 @@ public static void mainHelper(String[] args) throws IOException { LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.test.SampleTester", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.test.SampleTester", args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -119,11 +118,7 @@ public static void mainHelper(String[] args) throws IOException { // long option case 0: String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - - } else if (Daikon.config_option_SWITCH.equals(option_name)) { + if (Daikon.config_option_SWITCH.equals(option_name)) { String item = Daikon.getOptarg(g); daikon.config.Configuration.getInstance().apply(item); break; @@ -141,14 +136,10 @@ public static void mainHelper(String[] args) throws IOException { "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error); } } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 8711ac663f..1ffbe3b268 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -256,31 +256,29 @@ private static String getTestClassText() { ps.println(" }"); ps.println(); ps.println(" public static void mainHelper(String[] args) {"); - ps.println(" LongOpt[] longopts ="); - ps.println(" new LongOpt[] {new LongOpt(\"help\", LongOpt.NO_ARGUMENT, null, 'h')};"); ps.println(" DaikonGetopt g ="); + ps.println(" new DaikonGetopt("); ps.println( - " new DaikonGetopt(\"daikon.test.split.SplitterFactoryTest\", args, \"hs\"," - + " longopts);"); + " \"daikon.test.split.SplitterFactoryTest\", args, \"s\", new LongOpt[0]," + + " usage);"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); ps.println(" switch (c) {"); ps.println(" case 's':"); ps.println(" saveFiles = true;"); ps.println(" break;"); - ps.println(" case 'h':"); - ps.println(" System.out.println(usage);"); - ps.println(" throw new Daikon.NormalTermination();"); ps.println(" default:"); ps.println(" throw new Daikon.BugInDaikon(\"getopt() returned \" + c);"); ps.println(" }"); ps.println(" }"); ps.println(" Result result = JUnitCore.runClasses(SplitterFactoryTest.class);"); ps.println(" for (Failure failure : result.getFailures()) {"); - ps.println(" System.out.println(failure);"); + ps.println(" System.out.println(failure.getTrace());"); ps.println(" }"); ps.println(" if (!result.wasSuccessful()) {"); - ps.println(" throw new Daikon.UserError(result.getFailureCount() + \" tests failed\");"); + ps.println( + " throw new Daikon.UserError(StringsPlume.nPlural(result.getFailureCount(), \"test\")" + + " + \" failed\");"); ps.println(" }"); ps.println(" }"); ps.println(); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 864df768c1..1c5dbc4a0f 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -115,7 +115,6 @@ public static void mainHelper(final String[] args) { LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), // Process only part of the trace file new LongOpt(Daikon.ppt_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.ppt_omit_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -126,7 +125,7 @@ public static void mainHelper(final String[] args) { new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.DtraceDiff", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.DtraceDiff", args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -134,10 +133,7 @@ public static void mainHelper(final String[] args) { // long option case 0: String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { + if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { if (Daikon.ppt_regexp != null) { throw new Error( "multiple --" @@ -226,14 +222,10 @@ public static void mainHelper(final String[] args) { Configuration.getInstance().apply(item); break; } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } // short options - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/tools/DtraceNonceFixer.java b/java/daikon/tools/DtraceNonceFixer.java index 942ed31dca..d6703ba91a 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("daikon.tools.DtraceNonceFixer", 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/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index 134680f061..be5b17c7ed 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -114,23 +114,19 @@ public static void mainHelper(final String[] args) throws IOException { daikon.LogHelper.setupLogs(INFO); LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.ExtractConsequent", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.ExtractConsequent", args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { + if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { Daikon.suppress_redundant_invariants_with_simplify = true; } else if (Daikon.config_option_SWITCH.equals(option_name)) { String item = Daikon.getOptarg(g); @@ -141,12 +137,9 @@ public static void mainHelper(final String[] args) throws IOException { } else if (Daikon.debug_SWITCH.equals(option_name)) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index a44487ad54..2bb652ddb4 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -128,7 +128,6 @@ public static void mainHelper(final String[] args) LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(output_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(dir_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -137,20 +136,16 @@ public static void mainHelper(final String[] args) new LongOpt(verbose_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt(Daikon.ppt_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.InvariantChecker", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.InvariantChecker", args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (conf_SWITCH.equals(option_name)) { + if (conf_SWITCH.equals(option_name)) { doConf = true; } else if (filter_SWITCH.equals(option_name)) { doFilter = true; @@ -181,12 +176,9 @@ public static void mainHelper(final String[] args) "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error); } } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/tools/ReadTrace.java b/java/daikon/tools/ReadTrace.java index b47369d795..f237805b14 100644 --- a/java/daikon/tools/ReadTrace.java +++ b/java/daikon/tools/ReadTrace.java @@ -1,5 +1,7 @@ package daikon.tools; +import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.PptMap; import daikon.PptTopLevel; @@ -32,22 +34,47 @@ */ 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"); } + /** + * Returns the trace files named on the command line. Exits if the command line is bad or requests + * the usage message. + * + * @param args the command-line arguments + * @return the trace files named on the command line + */ + private static String[] traceFiles(String[] args) { + try { + String[] files = DaikonGetopt.nonOptionArgs("daikon.tools.ReadTrace", args, usage); + if (files.length == 0) { + throw new Daikon.UserError("No trace files specified" + Daikon.lineSep + usage); + } + return files; + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + throw new Error("unreachable"); + } + } + /** * The entry point for ReadTrace. * * @param args data trace file names, read from the command line */ public static void main(String[] args) { + String[] traceFiles = traceFiles(args); 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); + FileIO.read_data_trace_files( + new ArrayList<>(Arrays.asList(traceFiles)), ppts, processor, false); } catch (Exception e) { throw new Error(e); } diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 22f181956b..c552ecf817 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -87,12 +87,21 @@ public static void main(String[] args) { */ public static void mainHelper(final String[] args) { argles = args; - if (args.length == 0) { - throw new daikon.Daikon.UserError("No arguments found." + daikon.Daikon.lineSep + usage); + if (args.length > 0 && daikon.Daikon.isHelpArg(args[0])) { + System.out.println(usage); + throw new daikon.Daikon.NormalTermination(); + } + if (args.length < 2) { + throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); } - num_reps = Integer.parseInt(args[0]); - numPerSample = Integer.parseInt(args[1]); + try { + num_reps = Integer.parseInt(args[0]); + numPerSample = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + throw new daikon.Daikon.UserError( + "num_reps and sample_size must be integers." + daikon.Daikon.lineSep + usage); + } // process optional switches // also deduce index of arg for Daikon diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index bbffbe6758..e5c0073194 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -659,7 +659,6 @@ public static void mainHelper(final String[] args) new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt("filters", LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt("help", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("minimize-classes", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("no-post-after-pre-failure", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("post-after-pre-failure", LongOpt.NO_ARGUMENT, null, 0), @@ -676,7 +675,8 @@ public static void mainHelper(final String[] args) extra_assumptions = new LinkedHashMap<>(); - DaikonGetopt g = new DaikonGetopt("daikon.tools.compare.LogicalCompare", args, "h", longopts); + DaikonGetopt g = + new DaikonGetopt("daikon.tools.compare.LogicalCompare", args, "", longopts, usage); int c; boolean user_filters = false; while ((c = g.getopt()) != -1) { @@ -684,10 +684,7 @@ public static void mainHelper(final String[] args) case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (option_name.equals("config-file")) { + if (option_name.equals("config-file")) { String config_file = Daikon.getOptarg(g); try (InputStream stream = new FileInputStream(config_file)) { Configuration.getInstance().apply(stream); @@ -728,12 +725,9 @@ public static void mainHelper(final String[] args) } else if (option_name.equals("minimize-classes")) { opt_minimize_classes = true; } else { - throw new Error(); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index a369cf722a..67a24ed50d 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -132,7 +132,6 @@ public static void mainHelper(final String[] args) throws Exception { Daikon.output_format = OutputFormat.ESCJAVA; LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.format_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -140,7 +139,7 @@ public static void mainHelper(final String[] args) throws Exception { new LongOpt(max_invariants_pp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(no_reflection_SWITCH, LongOpt.NO_ARGUMENT, null, 0) }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.Annotate", args, "hs", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.Annotate", args, "s", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -148,10 +147,7 @@ public static void mainHelper(final String[] args) throws Exception { // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (no_reflection_SWITCH.equals(option_name)) { + if (no_reflection_SWITCH.equals(option_name)) { useReflection = false; } else if (max_invariants_pp_SWITCH.equals(option_name)) { try { @@ -181,12 +177,9 @@ public static void mainHelper(final String[] args) throws Exception { setLightweight = true; } } else { - throw new Daikon.UserError("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'i': insert_inexpressible = true; break; diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index 0d9ff03898..a6ddec8cac 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -83,35 +83,28 @@ public static void mainHelper(final String[] args) throws IOException { daikon.LogHelper.setupLogs(INFO); LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.CreateSpinfo", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.CreateSpinfo", args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.debugAll_SWITCH.equals(option_name)) { + if (Daikon.debugAll_SWITCH.equals(option_name)) { Global.debugAll = true; } else if (Daikon.debug_SWITCH.equals(option_name)) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; case 'o': outputfilename = Daikon.getOptarg(g); break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/tools/jtb/CreateSpinfoC.java b/java/daikon/tools/jtb/CreateSpinfoC.java index 8cb338947b..189e16e259 100644 --- a/java/daikon/tools/jtb/CreateSpinfoC.java +++ b/java/daikon/tools/jtb/CreateSpinfoC.java @@ -2,6 +2,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import daikon.Daikon; +import daikon.DaikonGetopt; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -21,21 +23,38 @@ private CreateSpinfoC() { throw new UnsupportedOperationException("Do not instantiate"); } - public static void main(String[] args) { - if (args.length == 1) { - System.out.println("Create spinfo file from file " + args[0] + " . . ."); - } else { - System.out.println("C Parser Version 0.1Alpha: Usage is one of:"); - System.out.println(" java CreateSpinfoC < inputfile"); - System.out.println("OR"); - System.out.println(" java CreateSpinfoC inputfile"); - return; + /** The usage message for this program. */ + private static final String usage = + "C Parser Version 0.1Alpha: Usage: java daikon.tools.jtb.CreateSpinfoC inputfile"; + + /** + * Returns the input file named on the command line. Exits if the command line is bad or requests + * the usage message. + * + * @param args the command-line arguments + * @return the input file named on the command line + */ + private static String inputFile(String[] args) { + try { + String[] files = DaikonGetopt.nonOptionArgs("daikon.tools.jtb.CreateSpinfoC", args, usage); + if (files.length != 1) { + throw new Daikon.UserError(usage); + } + return files[0]; + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + throw new Error("unreachable"); } + } + + public static void main(String[] args) { + String inputFile = inputFile(args); + System.out.println("Create spinfo file from file " + inputFile + " . . ."); try { - String fileName = args[0].substring(0, args[0].lastIndexOf('.')); + String fileName = inputFile.substring(0, inputFile.lastIndexOf('.')); File temp = new File(fileName + ".temp"); // filter out the '\f' characters in the file - try (Reader reader = Files.newBufferedReader(Paths.get(args[0]), UTF_8); + try (Reader reader = Files.newBufferedReader(Paths.get(inputFile), UTF_8); Writer writer = Files.newBufferedWriter(temp.toPath(), UTF_8)) { int c; while ((c = reader.read()) != -1) { diff --git a/java/daikon/tools/runtimechecker/CommandHandler.java b/java/daikon/tools/runtimechecker/CommandHandler.java index d00e0f2a15..3e733d6219 100644 --- a/java/daikon/tools/runtimechecker/CommandHandler.java +++ b/java/daikon/tools/runtimechecker/CommandHandler.java @@ -40,7 +40,8 @@ public void usageMessage(PrintStream out) { String docFile = simpleClassname + ".doc"; InputStream in = getClass().getResourceAsStream(docFile); if (in == null) { - out.println("Didn't find documentation for " + getClass()); + // This is an error message, so it goes to standard error even if `out` is standard output. + System.err.println("Didn't find documentation " + docFile + " for " + getClass()); return; } try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index c8205103c9..a4162b7497 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -85,7 +85,13 @@ public boolean handle(String[] args) { for (int i = 0; i < realArgs.length; i++) { realArgs[i] = args[i + 1]; } - Arguments arguments = readArguments(realArgs); + Arguments arguments; + try { + arguments = readArguments(realArgs); + } catch (Daikon.UserError e) { + System.err.println(e.getMessage()); + return false; + } if (arguments == errorWhileReadingArguments) { return false; } @@ -225,16 +231,7 @@ private Arguments readArguments(String[] args) { // The caller prints the usage message when this command fails. g.setUsageHint(null); int c; - while (true) { - try { - c = g.getopt(); - } catch (Daikon.UserError e) { - System.err.println(e.getMessage()); - return errorWhileReadingArguments; - } - if (c == -1) { - break; - } + while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option @@ -255,16 +252,14 @@ private Arguments readArguments(String[] args) { } else if (Daikon.debug_SWITCH.equals(option_name)) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { - System.err.println("Unknown long option received: " + option_name); - return errorWhileReadingArguments; + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; case 'h': usageMessage(System.out); return helpRequested; default: - System.err.println("getopt() returned " + c); - return errorWhileReadingArguments; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } // The index of the first non-option argument -- the name of the diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 45a59c4eda..4e7272e85f 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,5 +1,6 @@ package daikon.tools.runtimechecker; +import daikon.Daikon; import java.io.PrintStream; import java.util.Collections; import java.util.List; @@ -40,7 +41,9 @@ public void nonStaticMain(String[] args) { "For more help, invoke the instrumenter with \"help\" as its sole argument."); System.exit(1); } - if (args[0].toUpperCase(Locale.ENGLISH).equals("HELP") || args[0].equals("?")) { + if (args[0].toUpperCase(Locale.ENGLISH).equals("HELP") + || args[0].equals("?") + || Daikon.isHelpArg(args[0])) { usageMessage(System.out); usageMessage(handlers, System.out); System.exit(0); diff --git a/java/daikon/tools/runtimechecker/WriteViolationFile.java b/java/daikon/tools/runtimechecker/WriteViolationFile.java index dcfb548648..96ca39d574 100644 --- a/java/daikon/tools/runtimechecker/WriteViolationFile.java +++ b/java/daikon/tools/runtimechecker/WriteViolationFile.java @@ -2,6 +2,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import daikon.Daikon; import java.io.BufferedWriter; import java.io.IOException; import java.io.UncheckedIOException; @@ -41,6 +42,10 @@ public static void main(String[] args) { usage(); System.exit(1); } + if (Daikon.isHelpArg(args[0])) { + usage(); + System.exit(0); + } @SuppressWarnings("signature") // will be checked immediately below, and exception is caught @BinaryName String class_name = args[0]; String[] main_args = new String[args.length - 1]; From f8ceb8b9f4e898df7b292f89b98bb4e58905a7a0 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 15:17:23 -0700 Subject: [PATCH 10/15] Fix CI: nullness error in DaikonGetopt.withHelp; put java-getopt on DOCLETPATH ParameterDoclet loads daikon.Daikon reflectively, and verifying Daikon now requires loading DaikonGetopt and its superclass gnu.getopt.Getopt. Co-Authored-By: Claude Opus 5.5 --- java/Makefile | 2 +- java/daikon/DaikonGetopt.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java/Makefile b/java/Makefile index a259f02945..675725091f 100644 --- a/java/Makefile +++ b/java/Makefile @@ -181,7 +181,7 @@ JAVADOC_COMMAND := ${JAVADOC} -cp ${DAIKON_CLASSPATH} JAVAC_COMMAND := ${JAVAC} -cp ${DAIKON_CLASSPATH} -Xlint -J-Xmx7g ${JAVAC_ARGS} JAVA_COMMAND := ${JAVA} -cp ${DAIKON_CLASSPATH} -DOCLETPATH := ${DAIKONDIR}/java:$(wildcard ${DAIKONDIR}/java/lib/bcel-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/plume-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/reflection-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/hashmap-util-*.jar):${DAIKONDIR}/java/lib/daikon-plumelib.jar +DOCLETPATH := ${DAIKONDIR}/java:$(wildcard ${DAIKONDIR}/java/lib/java-getopt-*.jar):$(wildcard ${DAIKONDIR}/java/lib/bcel-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/plume-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/reflection-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/hashmap-util-*.jar):${DAIKONDIR}/java/lib/daikon-plumelib.jar DOCLET_FLAGS := LINKOFFLINE := https://docs.oracle.com/javase/8/docs/api/ diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 66ff21360d..fab4e1ecf4 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -4,6 +4,7 @@ import gnu.getopt.LongOpt; import java.util.Arrays; import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.util.ArraysPlume; /** * A command-line option processor that throws a {@link Daikon.UserError} that describes a bad @@ -61,9 +62,8 @@ public DaikonGetopt( * @return {@code longopts} plus {@code --help} */ private static LongOpt[] withHelp(LongOpt[] longopts) { - LongOpt[] result = Arrays.copyOf(longopts, longopts.length + 1); - result[longopts.length] = new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h'); - return result; + return ArraysPlume.append( + longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } /** From 46b4a73cfc6ead1f4f7e08b6f65c1ed8754334ca Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 15:30:43 -0700 Subject: [PATCH 11/15] Address code review: Annotate -i, TraceSelect bad options, help via DaikonGetopt * Annotate accepts -i again; drop the unimplemented -r from its usage message. * TraceSelect rejects unrecognized switches, accepts -h/--help among its options, and reports a non-integer -SEED argument as a user error. * Replace Daikon.isHelpArg with DaikonGetopt.argsAfterLeadingOptions, used by TraceSelect, WriteViolationFile, and runtimechecker.Main. * ReadTrace and CreateSpinfoC use the main/mainHelper split. * Remove the orphaned StackAr-u.txt.goal. Co-Authored-By: Claude Opus 5.5 --- java/daikon/Daikon.java | 10 -- java/daikon/DaikonGetopt.java | 37 +++++- java/daikon/tools/ReadTrace.java | 27 ++--- java/daikon/tools/TraceSelect.java | 35 ++++-- java/daikon/tools/jtb/Annotate.java | 3 +- java/daikon/tools/jtb/CreateSpinfoC.java | 37 +++--- java/daikon/tools/runtimechecker/Main.java | 41 +++++-- .../runtimechecker/WriteViolationFile.java | 39 ++++--- tests/diff-tests/StackAr/StackAr-u.txt.goal | 105 ------------------ 9 files changed, 151 insertions(+), 183 deletions(-) delete mode 100644 tests/diff-tests/StackAr/StackAr-u.txt.goal diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 7a9952b75c..6dfa380ff9 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1485,16 +1485,6 @@ public static String getOptarg(Getopt g) { return result; } - /** - * Returns true if the command-line argument requests a usage message. - * - * @param arg a command-line argument - * @return true if {@code arg} is "-h" or "--help" - */ - public static boolean isHelpArg(String arg) { - return arg.equals("-h") || arg.equals("--" + help_SWITCH); - } - /** * Invariants passed on the command line with the {@code --user_defined_invariant} option. A list * of class names in the format required by {@link Class#forName(String)}. diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index fab4e1ecf4..0dad85d21f 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -78,7 +78,42 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { * @throws Daikon.UserError if an argument is any other option */ public static String[] nonOptionArgs(String progname, String[] argv, String usage) { - DaikonGetopt g = new DaikonGetopt(progname, argv, "", new LongOpt[0], usage); + return argsAfterOptions(progname, argv, "", usage); + } + + /** + * Processes the leading command-line arguments of a program that takes no options other than + * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so + * later arguments may be options for some other program. + * + * @param progname the name of the program, for use in messages + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String progname, String[] argv, String usage) { + // A leading "+" tells Getopt to stop at the first argument that is not an option. + return argsAfterOptions(progname, argv, "+", usage); + } + + /** + * Processes command-line arguments, permitting no options other than {@code -h} and {@code + * --help}. + * + * @param progname the name of the program, for use in messages + * @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 progname, String[] argv, String optstring, String usage) { + DaikonGetopt g = new DaikonGetopt(progname, argv, 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) { diff --git a/java/daikon/tools/ReadTrace.java b/java/daikon/tools/ReadTrace.java index f237805b14..a185839ebc 100644 --- a/java/daikon/tools/ReadTrace.java +++ b/java/daikon/tools/ReadTrace.java @@ -43,32 +43,29 @@ private ReadTrace() { } /** - * Returns the trace files named on the command line. Exits if the command line is bad or requests - * the usage message. + * The entry point for ReadTrace. * - * @param args the command-line arguments - * @return the trace files named on the command line + * @param args data trace file names, read from the command line */ - private static String[] traceFiles(String[] args) { + public static void main(String[] args) { try { - String[] files = DaikonGetopt.nonOptionArgs("daikon.tools.ReadTrace", args, usage); - if (files.length == 0) { - throw new Daikon.UserError("No trace files specified" + Daikon.lineSep + usage); - } - return files; + mainHelper(args); } catch (Daikon.DaikonTerminationException e) { Daikon.handleDaikonTerminationException(e); - throw new Error("unreachable"); } } /** - * The entry point for ReadTrace. + * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is + * appropriate to be called programmatically. * - * @param args data trace file names, read from the command line + * @param args command-line arguments, like those of {@link #main} */ - public static void main(String[] args) { - String[] traceFiles = traceFiles(args); + public static void mainHelper(String[] args) { + String[] traceFiles = DaikonGetopt.nonOptionArgs("daikon.tools.ReadTrace", 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 { diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index c552ecf817..5778c4d294 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -1,6 +1,7 @@ // TraceSelect.java package daikon.tools; +import daikon.DaikonGetopt; import java.io.File; import java.io.IOException; import java.io.PrintWriter; @@ -59,11 +60,16 @@ private TraceSelect() { // Always set to non-null by mainHelper private static String @MonotonicNonNull [] sampleNames; + /** The name of this program, for use in messages. */ + private static final String progname = "daikon.tools.TraceSelect"; + /** The usage message for this program. */ private static final String usage = StringsPlume.joinLines( "USAGE: TraceSelect num_reps sample_size [options] [Daikon-args]...", - "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED-SEED 1000 foo.dtrace" + "The options are -SEED n, -NOCLEAN, -INCLUDE_UNRETURNED, and -DO_DIFFS.", + "The Daikon-args start with the first .dtrace or .decls file.", + "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED -SEED 1000 foo.dtrace" + " foo2.dtrace foo.decls RatPoly.decls foo3.dtrace"); /** @@ -85,12 +91,10 @@ public static void main(String[] args) { * * @param args command-line arguments, like those of {@link #main} */ - public static void mainHelper(final String[] args) { + public static void mainHelper(String[] args) { + // Handles -h and --help before num_reps. + args = DaikonGetopt.argsAfterLeadingOptions(progname, args, usage); argles = args; - if (args.length > 0 && daikon.Daikon.isHelpArg(args[0])) { - System.out.println(usage); - throw new daikon.Daikon.NormalTermination(); - } if (args.length < 2) { throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); } @@ -110,9 +114,14 @@ public static void mainHelper(final String[] args) { // allows seed setting if (args[i].toUpperCase(Locale.ENGLISH).equals("-SEED")) { if (i + 1 >= args.length) { - throw new daikon.Daikon.UserError("-SEED options requires argument"); + throw new daikon.Daikon.UserError("-SEED requires an argument"); + } + String seed = args[++i]; + try { + randObj = new Random(Long.parseLong(seed)); + } catch (NumberFormatException e) { + throw new daikon.Daikon.UserError("-SEED requires an integer argument, not " + seed); } - randObj = new Random(Long.parseLong(args[++i])); daikonArgStart = i + 1; } @@ -167,6 +176,16 @@ else if (args[i].endsWith(".dtrace")) { knowArgStart = true; } } + + // Any other switch before the Daikon arguments is a request for help or an error. + else if (!knowArgStart && args[i].startsWith("-")) { + if (args[i].startsWith("--") || args[i].length() == 2) { + // Prints the usage message for -h and --help, and describes any other such option. + DaikonGetopt.nonOptionArgs(progname, new String[] {args[i]}, usage); + } + throw new daikon.Daikon.UserError( + "Unrecognized command-line option " + args[i] + "; run with -h for usage"); + } } // if no seed provided, use default Random() constructor diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 67a24ed50d..485427045c 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -91,7 +91,6 @@ private Annotate() { " -h Display this usage message", " -i Insert invariants not supported by ESC with \"!\" instead of \"@\";", " by default these \"inexpressible\" invariants are simply omitted", - " -r Use all .java files under the current directory as arguments", " -s Use // comments rather than /* comments", " --format name Insert specifications in the given format: DBC, ESC, JML, Java", " --wrap_xml Wrap each annotation and auxiliary information in XML tags", @@ -139,7 +138,7 @@ public static void mainHelper(final String[] args) throws Exception { new LongOpt(max_invariants_pp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(no_reflection_SWITCH, LongOpt.NO_ARGUMENT, null, 0) }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.Annotate", args, "s", longopts, usage); + DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.Annotate", args, "is", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/tools/jtb/CreateSpinfoC.java b/java/daikon/tools/jtb/CreateSpinfoC.java index 189e16e259..51c0f1a147 100644 --- a/java/daikon/tools/jtb/CreateSpinfoC.java +++ b/java/daikon/tools/jtb/CreateSpinfoC.java @@ -28,27 +28,30 @@ private CreateSpinfoC() { "C Parser Version 0.1Alpha: Usage: java daikon.tools.jtb.CreateSpinfoC inputfile"; /** - * Returns the input file named on the command line. Exits if the command line is bad or requests - * the usage message. + * The entry point for CreateSpinfoC. * - * @param args the command-line arguments - * @return the input file named on the command line + * @param args one argument, the name of the C file */ - private static String inputFile(String[] args) { + public static void main(String[] args) { try { - String[] files = DaikonGetopt.nonOptionArgs("daikon.tools.jtb.CreateSpinfoC", args, usage); - if (files.length != 1) { - throw new Daikon.UserError(usage); - } - return files[0]; + mainHelper(args); } catch (Daikon.DaikonTerminationException e) { Daikon.handleDaikonTerminationException(e); - throw new Error("unreachable"); } } - public static void main(String[] args) { - String inputFile = inputFile(args); + /** + * 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[] files = DaikonGetopt.nonOptionArgs("daikon.tools.jtb.CreateSpinfoC", args, usage); + if (files.length != 1) { + throw new Daikon.UserError(usage); + } + String inputFile = files[0]; System.out.println("Create spinfo file from file " + inputFile + " . . ."); try { String fileName = inputFile.substring(0, inputFile.lastIndexOf('.')); @@ -63,12 +66,8 @@ public static void main(String[] args) { } } } catch (IOException e) { - System.out.println(e.getMessage()); - if (temp != null) { - temp.delete(); - } - System.exit(1); - throw new Error("unreachable"); + temp.delete(); + throw new Daikon.UserError(e, "Problem copying " + inputFile + " to " + temp); } try (FileInputStream fis = new FileInputStream(temp)) { @SuppressWarnings("UnusedVariable") // sets static variables for TranslationUnit() diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 4e7272e85f..d6f7f20f8e 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,6 +1,10 @@ package daikon.tools.runtimechecker; +import static java.nio.charset.StandardCharsets.UTF_8; + import daikon.Daikon; +import daikon.DaikonGetopt; +import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Collections; import java.util.List; @@ -24,6 +28,20 @@ protected void usageMessage(List handlers, PrintStream out) { } } + /** + * Returns the usage message of this and of each handler. + * + * @param handlers the handlers whose usage messages to include + * @return the usage message of this and of each handler + */ + private String usageMessage(List handlers) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bytes, true, UTF_8); + usageMessage(out); + usageMessage(handlers, out); + return bytes.toString(UTF_8).stripTrailing(); + } + /** * Entry point for the instrumenter. Passes control to whichever handler can handle the * user-specified command. @@ -35,21 +53,28 @@ public void nonStaticMain(String[] args) { List handlers = Collections.singletonList((CommandHandler) new InstrumentHandler()); - if (args.length < 1) { + String[] commandAndArgs; + try { + commandAndArgs = + DaikonGetopt.argsAfterLeadingOptions( + "daikon.tools.runtimechecker.Main", args, usageMessage(handlers)); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + return; + } + if (commandAndArgs.length < 1) { System.err.println("ERROR: No command given."); System.err.println( "For more help, invoke the instrumenter with \"help\" as its sole argument."); System.exit(1); } - if (args[0].toUpperCase(Locale.ENGLISH).equals("HELP") - || args[0].equals("?") - || Daikon.isHelpArg(args[0])) { - usageMessage(System.out); - usageMessage(handlers, System.out); + if (commandAndArgs[0].toUpperCase(Locale.ENGLISH).equals("HELP") + || commandAndArgs[0].equals("?")) { + System.out.println(usageMessage(handlers)); System.exit(0); } - String command = args[0]; + String command = commandAndArgs[0]; boolean success = false; @@ -59,7 +84,7 @@ public void nonStaticMain(String[] args) { for (CommandHandler handler : handlers) { if (handler.handles(command)) { h = handler; - success = h.handle(args); + success = h.handle(commandAndArgs); if (!success) { System.err.println("The command you issued returned a failing status flag."); } diff --git a/java/daikon/tools/runtimechecker/WriteViolationFile.java b/java/daikon/tools/runtimechecker/WriteViolationFile.java index 96ca39d574..4f5070c828 100644 --- a/java/daikon/tools/runtimechecker/WriteViolationFile.java +++ b/java/daikon/tools/runtimechecker/WriteViolationFile.java @@ -3,6 +3,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import daikon.Daikon; +import daikon.DaikonGetopt; import java.io.BufferedWriter; import java.io.IOException; import java.io.UncheckedIOException; @@ -12,8 +13,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.List; import org.checkerframework.checker.signature.qual.BinaryName; +import org.plumelib.util.StringsPlume; /** * This program runs a given program (which is assumed to contain some classes that have been @@ -27,31 +30,37 @@ private WriteViolationFile() { throw new UnsupportedOperationException("Do not instantiate"); } + /** The usage message for this program. */ + private static final String usageMessage = + StringsPlume.joinLines( + "Usage: java WriteViolationFile CLASS ARGS", + " CLASS and ARGS are just as they would be when being run directly,", + " except that CLASS is written as a binary name, not a fully-qualified name", + "Output is written to file \"violations.txt\" in the current directory."); + public static void usage() { - System.out.println("Usage: java WriteViolationFile CLASS ARGS"); - System.out.println(" CLASS and ARGS are just as they would be when being run directly,"); - System.out.println( - " except that CLASS is written as a binary name, not a fully-qualified name"); - System.out.println("Output is written to file \"violations.txt\" in the current directory."); + System.out.println(usageMessage); } @SuppressWarnings("Finally") public static void main(String[] args) { - if (args.length == 0) { + String[] classAndArgs; + try { + classAndArgs = + DaikonGetopt.argsAfterLeadingOptions( + "daikon.tools.runtimechecker.WriteViolationFile", args, usageMessage); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + return; + } + if (classAndArgs.length == 0) { System.out.println("Error: no class specified"); usage(); System.exit(1); } - if (Daikon.isHelpArg(args[0])) { - usage(); - System.exit(0); - } @SuppressWarnings("signature") // will be checked immediately below, and exception is caught - @BinaryName String class_name = args[0]; - String[] main_args = new String[args.length - 1]; - for (int i = 0; i < main_args.length; i++) { - main_args[i] = args[i + 1]; - } + @BinaryName String class_name = classAndArgs[0]; + String[] main_args = Arrays.copyOfRange(classAndArgs, 1, classAndArgs.length); Class cls; try { cls = Class.forName(class_name); diff --git a/tests/diff-tests/StackAr/StackAr-u.txt.goal b/tests/diff-tests/StackAr/StackAr-u.txt.goal deleted file mode 100644 index f3164de375..0000000000 --- a/tests/diff-tests/StackAr/StackAr-u.txt.goal +++ /dev/null @@ -1,105 +0,0 @@ -(I)V:::ENTER> - = 0 {0+}, capacity >= 2 {1-}> (UInt,DJU) - (U!Int,DUU) -(I)V:::EXIT> - (Bin,MJ) - (Bin,MJ) - = -1 {0.8136-}, this.topOfStack >= -1 {0.8333-}> (U!Int,SUU) - (UInt,SJU) - (U!Int,DUU) - (U!Int,SUU) - (UInt,SJU) - (UInt,MJ) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJU) - (U!Int,DUU) - (UInt,MJ) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJU) - (U!Int,DJU) - (U!Int,DUU) - (UInt,MJ) - - (Bin,MJ) - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJJ) - (U!Int,DUU) - - (Bin,MJ) - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (UInt,MJ) - (U!Int,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - (Bin,JM) - = -1 {0+}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,DJU) - (U!Int,UM) - (U!Int,DUU) - (UInt,JM) - (UInt,JM) - (UInt,DJU) - - (Bin,JM) - (Bin,JM) - (Bin,JM) - (Bin,JM) - (Bin,JM) - = -1 {0+}, null> (U!Int,JM) - = -1 {0.8205-}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,DUU) - (UInt,DJU) - (U!Int,UM) - (U!Int,UM) - (U!Int,UM) - (U!Int,DUU) - (UInt,DJU) - (UInt,JM) - (UInt,JM) - (UInt,DJU) - (UInt,DJU) - - = 9223372036854775807 {1-}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,SUU) - (U!Int,SUU) - - = 9223372036854775807 {1-}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,SUU) - (U!Int,SUU) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJJ) - (U!Int,DUU) - - = -1 {0+}, orig(this.topOfStack) >= -1 {0+}> (U!Int,SJJ) - (U!Int,DUU) - (U!Int,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - = -1 {0+}, this.topOfStack >= 0 {0+}> (UInt,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - (Bin,MJ) - (Bin,MJ) - (Bin,MJ) - (Bin,MJ) - = -1 {0+}, this.topOfStack >= 0 {0+}> (UInt,DJJ) - (UInt,MJ) - (U!Int,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJU) - (U!Int,DUU) - - (Bin,MJ) - (Bin,MJ) - (Bin,MJ) - = -1 {0+}, orig(this.topOfStack) >= -1 {0+}> (U!Int,SJJ) - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DUU) - (U!Int,DJU) - (U!Int,DJU) - (U!Int,DUU) From 6625eaa1f0640c412ebd5b2e7ec43573f1bcf315 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 16:12:36 -0700 Subject: [PATCH 12/15] Address code review: CreateSpinfoC, TraceSelect, DaikonGetopt cleanups - CreateSpinfoC: parse while the input stream is open; exit non-zero on parse or output errors; reject a file name with no extension. - TraceSelect: an unrecognized switch before the first .dtrace/.decls file starts the Daikon arguments instead of being an error; handle -h/--help directly. - DaikonGetopt: drop the unused progname parameter; accept a Supplier usage message so it is computed only when needed. - runtimechecker: compute Main's usage lazily; InstrumentHandler uses DaikonGetopt's help handling. - Annotate: remove stale -r from Javadoc. - BadOptionTest: capture usage output; test TraceSelect help. Co-Authored-By: Claude Opus 5.5 --- java/daikon/Daikon.java | 2 +- java/daikon/DaikonGetopt.java | 71 +++++++++++++------ java/daikon/MergeInvariants.java | 2 +- java/daikon/PrintInvariants.java | 2 +- java/daikon/SplitDtrace.java | 2 +- java/daikon/UnionInvariants.java | 2 +- java/daikon/diff/Diff.java | 3 +- java/daikon/test/BadOptionTest.java | 55 +++++++++----- java/daikon/test/SampleTester.java | 2 +- .../split/SplitterFactoryTestUpdater.java | 6 +- java/daikon/tools/DtraceDiff.java | 2 +- java/daikon/tools/DtraceNonceFixer.java | 2 +- java/daikon/tools/ExtractConsequent.java | 2 +- java/daikon/tools/InvariantChecker.java | 2 +- java/daikon/tools/ReadTrace.java | 2 +- java/daikon/tools/TraceSelect.java | 33 ++++----- java/daikon/tools/compare/LogicalCompare.java | 3 +- java/daikon/tools/jtb/Annotate.java | 5 +- java/daikon/tools/jtb/CreateSpinfo.java | 2 +- java/daikon/tools/jtb/CreateSpinfoC.java | 31 ++++---- .../tools/runtimechecker/CommandHandler.java | 12 ++++ .../runtimechecker/InstrumentHandler.java | 22 ++---- java/daikon/tools/runtimechecker/Main.java | 4 +- .../runtimechecker/WriteViolationFile.java | 4 +- 24 files changed, 157 insertions(+), 116 deletions(-) diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 6dfa380ff9..aaa2b3f3d6 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1054,7 +1054,7 @@ static FileOptions read_options(String[] args, String usage) { new LongOpt(disc_reason_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(mem_stat_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.Daikon", args, "o:", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 0dad85d21f..0f8b17cb05 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -3,6 +3,7 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.util.Arrays; +import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; @@ -16,10 +17,15 @@ public class DaikonGetopt extends Getopt { /** - * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles - * them. + * The name passed to {@link Getopt}, which uses it only in messages that DaikonGetopt suppresses. */ - private final @Nullable String usage; + private static final String PROGNAME = "DaikonGetopt"; + + /** + * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller + * handles them. + */ + private final @Nullable Supplier usage; /** Text appended to the description of a bad command-line option, or null to append nothing. */ private @Nullable String usageHint = "run with -h for usage"; @@ -27,13 +33,12 @@ public class DaikonGetopt extends Getopt { /** * Creates a command-line option processor that recognizes short and long options. * - * @param progname the name of the program, for use in messages * @param argv the command-line arguments * @param optstring the short options, in the format of {@link Getopt} * @param longopts the long options */ - public DaikonGetopt(String progname, String[] argv, String optstring, LongOpt[] longopts) { - super(progname, argv, optstring, longopts); + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { + super(PROGNAME, argv, optstring, longopts); this.usage = null; opterr = false; } @@ -42,15 +47,27 @@ public DaikonGetopt(String progname, String[] argv, String optstring, LongOpt[] * Creates a command-line option processor that recognizes short and long options, plus {@code -h} * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. * - * @param progname the name of the program, for use in messages * @param argv the command-line arguments * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} * @param longopts the long options; must not contain {@code help} * @param usage the usage message to print for {@code -h} and {@code --help} */ - public DaikonGetopt( - String progname, String[] argv, String optstring, LongOpt[] longopts, String usage) { - super(progname, argv, optstring + "h", withHelp(longopts)); + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { + this(argv, optstring, longopts, () -> usage); + } + + /** + * Creates a command-line option processor that recognizes short and long options, plus {@code -h} + * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. + * The usage message is computed only if it is needed. + * + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} + * @param longopts the long options; must not contain {@code help} + * @param usage computes the usage message to print for {@code -h} and {@code --help} + */ + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { + super(PROGNAME, argv, optstring + "h", withHelp(longopts)); this.usage = usage; opterr = false; } @@ -70,15 +87,14 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { * Processes the command-line arguments of a program that takes no options other than {@code -h} * and {@code --help}. * - * @param progname the name of the program, for use in messages * @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 progname, String[] argv, String usage) { - return argsAfterOptions(progname, argv, "", usage); + public static String[] nonOptionArgs(String[] argv, String usage) { + return argsAfterOptions(argv, "", () -> usage); } /** @@ -86,7 +102,6 @@ public static String[] nonOptionArgs(String progname, String[] argv, String usag * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so * later arguments may be options for some other program. * - * @param progname the name of the program, for use in messages * @param argv the command-line arguments * @param usage the usage message to print for {@code -h} and {@code --help} * @return the arguments from the first one that is not an option onward @@ -94,26 +109,40 @@ public static String[] nonOptionArgs(String progname, String[] argv, String usag * requests it * @throws Daikon.UserError if a leading argument is any other option */ - public static String[] argsAfterLeadingOptions(String progname, String[] argv, String usage) { + public static String[] argsAfterLeadingOptions(String[] argv, String usage) { + return argsAfterLeadingOptions(argv, () -> usage); + } + + /** + * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if + * it is needed. + * + * @param argv the command-line arguments + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { // A leading "+" tells Getopt to stop at the first argument that is not an option. - return argsAfterOptions(progname, argv, "+", usage); + return argsAfterOptions(argv, "+", usage); } /** * Processes command-line arguments, permitting no options other than {@code -h} and {@code * --help}. * - * @param progname the name of the program, for use in messages * @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} + * @param usage computes 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 progname, String[] argv, String optstring, String usage) { - DaikonGetopt g = new DaikonGetopt(progname, argv, optstring, new LongOpt[0], usage); + String[] argv, String optstring, Supplier usage) { + DaikonGetopt g = new DaikonGetopt(argv, 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) { @@ -150,7 +179,7 @@ public int getopt() { throw badOptionError(); } if (c == 'h' && usage != null) { - System.out.println(usage); + System.out.println(usage.get()); throw new Daikon.NormalTermination(); } return c; diff --git a/java/daikon/MergeInvariants.java b/java/daikon/MergeInvariants.java index 84c79dae0b..8a814a80fb 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -102,7 +102,7 @@ public static void mainHelper(String[] args) new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.MergeInvariants", args, "o:", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index b8df808759..57f483661d 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -342,7 +342,7 @@ public static void mainHelper(String[] args) new LongOpt( PrintInvariants.print_csharp_metadata_SWITCH, LongOpt.OPTIONAL_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.PrintInvariants", args, "", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/SplitDtrace.java b/java/daikon/SplitDtrace.java index ab93f582b3..aff7705ba9 100644 --- a/java/daikon/SplitDtrace.java +++ b/java/daikon/SplitDtrace.java @@ -58,7 +58,7 @@ public static void main(String[] args) throws IOException { * @throws IOException if there is a problem reading or writing a file */ public static void mainHelper(String[] args) throws IOException { - String[] files = DaikonGetopt.nonOptionArgs("daikon.SplitDtrace", args, usage); + String[] files = DaikonGetopt.nonOptionArgs(args, usage); if (files.length != 1) { throw new Daikon.UserError(usage); } diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index 1dd0f5c891..c07e19d7ce 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -51,7 +51,7 @@ public static void mainHelper(String[] args) throws Exception { new LongOpt[] { new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.UnionInvariants", args, "o:", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index e40e5ba988..6355ef0892 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -206,8 +206,7 @@ public static void mainHelper(final String[] args) new LongOpt(IGNORE_NUMBERED_EXITS_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - DaikonGetopt g = - new DaikonGetopt("daikon.diff.Diff", args, "Hydastmxno:jpevl", longOpts, usage); + DaikonGetopt g = new DaikonGetopt(args, "Hydastmxno:jpevl", longOpts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 41edce25be..67d118e64f 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -1,7 +1,9 @@ package daikon.test; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import daikon.Daikon; @@ -10,7 +12,10 @@ import daikon.SplitDtrace; import daikon.UnionInvariants; import daikon.tools.DtraceDiff; +import daikon.tools.TraceSelect; import gnu.getopt.LongOpt; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -40,7 +45,7 @@ public class BadOptionTest { * @return the message of the UserError for the bad option */ private static String parse(String... args) { - DaikonGetopt g = new DaikonGetopt("BadOptionTest", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "ho:", longopts); Daikon.UserError e = assertThrows(Daikon.UserError.class, () -> consumeAll(g)); return String.valueOf(e.getMessage()); } @@ -93,7 +98,7 @@ public void testAmbiguousLongOption() { /** Tests that a usage hint of null appends nothing to the description of a bad option. */ @Test public void testNoUsageHint() { - DaikonGetopt g = new DaikonGetopt("BadOptionTest", new String[] {"--bogus"}, "h", longopts); + DaikonGetopt g = new DaikonGetopt(new String[] {"--bogus"}, "h", longopts); g.setUsageHint(null); Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); assertEquals("Unrecognized command-line option --bogus", String.valueOf(e.getMessage())); @@ -111,11 +116,11 @@ public void testMissingArgument() { /** Tests options that are missing their required argument, when the optstring starts with ':'. */ @Test public void testMissingArgumentLeadingColon() { - DaikonGetopt g = new DaikonGetopt("BadOptionTest", new String[] {"-o"}, ":ho:", longopts); + DaikonGetopt g = new DaikonGetopt(new String[] {"-o"}, ":ho:", longopts); Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); assertEquals( expected("Command-line option -o requires an argument"), String.valueOf(e.getMessage())); - g = new DaikonGetopt("BadOptionTest", new String[] {"--config"}, ":ho:", longopts); + g = new DaikonGetopt(new String[] {"--config"}, ":ho:", longopts); e = assertThrows(Daikon.UserError.class, g::getopt); assertEquals( expected("Command-line option --config requires an argument"), @@ -153,12 +158,30 @@ public void testPrintInvariantsUnrecognizedOption() { /** Tests that tools accept the {@code --help} option that their usage messages document. */ @Test public void testHelpOption() { - assertThrows( - Daikon.NormalTermination.class, () -> DtraceDiff.mainHelper(new String[] {"--help"})); - assertThrows( - Daikon.NormalTermination.class, () -> UnionInvariants.mainHelper(new String[] {"--help"})); - assertThrows( - Daikon.NormalTermination.class, () -> SplitDtrace.mainHelper(new String[] {"-h", "x"})); + assertPrintsUsage(() -> DtraceDiff.mainHelper(new String[] {"--help"})); + assertPrintsUsage(() -> UnionInvariants.mainHelper(new String[] {"--help"})); + assertPrintsUsage(() -> SplitDtrace.mainHelper(new String[] {"-h", "x"})); + assertPrintsUsage(() -> TraceSelect.mainHelper(new String[] {"--help"})); + assertPrintsUsage(() -> TraceSelect.mainHelper(new String[] {"20", "10", "-h", "x.dtrace"})); + } + + /** + * Asserts that the given code prints a usage message to standard output and throws {@link + * Daikon.NormalTermination}. Captures the usage message so that it does not clutter the test + * output. + * + * @param code the code to run + */ + private static void assertPrintsUsage(ThrowingRunnable code) { + PrintStream oldOut = System.out; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bytes, true, UTF_8)); + try { + assertThrows(Daikon.NormalTermination.class, code); + } finally { + System.setOut(oldOut); + } + assertNotEquals("", bytes.toString(UTF_8).trim()); } /** Tests {@link DaikonGetopt#nonOptionArgs}. */ @@ -166,18 +189,16 @@ public void testHelpOption() { public void testNonOptionArgs() { assertArrayEquals( new String[] {"a.dtrace", "b.dtrace"}, - DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"a.dtrace", "b.dtrace"}, "u")); + DaikonGetopt.nonOptionArgs(new String[] {"a.dtrace", "b.dtrace"}, "u")); assertArrayEquals( new String[] {"-a.dtrace"}, - DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"--", "-a.dtrace"}, "u")); + DaikonGetopt.nonOptionArgs(new String[] {"--", "-a.dtrace"}, "u")); assertBadOption( "Unrecognized command-line option --bogus", - () -> DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"x", "--bogus"}, "u")); + () -> DaikonGetopt.nonOptionArgs(new String[] {"x", "--bogus"}, "u")); assertBadOption( "Unrecognized command-line option -x", - () -> DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"-x"}, "u")); - assertThrows( - Daikon.NormalTermination.class, - () -> DaikonGetopt.nonOptionArgs("BadOptionTest", new String[] {"x", "--help"}, "u")); + () -> DaikonGetopt.nonOptionArgs(new String[] {"-x"}, "u")); + assertPrintsUsage(() -> DaikonGetopt.nonOptionArgs(new String[] {"x", "--help"}, "u")); } } diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index 17718e4840..b984841454 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -110,7 +110,7 @@ public static void mainHelper(String[] args) throws IOException { new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.test.SampleTester", args, "", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 1ffbe3b268..c5be608fc7 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -256,11 +256,7 @@ private static String getTestClassText() { ps.println(" }"); ps.println(); ps.println(" public static void mainHelper(String[] args) {"); - ps.println(" DaikonGetopt g ="); - ps.println(" new DaikonGetopt("); - ps.println( - " \"daikon.test.split.SplitterFactoryTest\", args, \"s\", new LongOpt[0]," - + " usage);"); + ps.println(" DaikonGetopt g = new DaikonGetopt(args, \"s\", new LongOpt[0], usage);"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); ps.println(" switch (c) {"); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 1c5dbc4a0f..e1c7bbce3f 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -125,7 +125,7 @@ public static void mainHelper(final String[] args) { new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.DtraceDiff", args, "", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/tools/DtraceNonceFixer.java b/java/daikon/tools/DtraceNonceFixer.java index d6703ba91a..273e0de541 100644 --- a/java/daikon/tools/DtraceNonceFixer.java +++ b/java/daikon/tools/DtraceNonceFixer.java @@ -54,7 +54,7 @@ public static void main(String[] args) { * @param args command-line arguments, like those of {@link #main} */ public static void mainHelper(final String[] args) { - String[] files = DaikonGetopt.nonOptionArgs("daikon.tools.DtraceNonceFixer", args, usage); + String[] files = DaikonGetopt.nonOptionArgs(args, usage); if (files.length != 1) { throw new daikon.Daikon.UserError(usage); } diff --git a/java/daikon/tools/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index be5b17c7ed..d4f94d9f49 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -119,7 +119,7 @@ public static void mainHelper(final String[] args) throws IOException { new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.ExtractConsequent", args, "", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index 2bb652ddb4..3f8c7c56b7 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -138,7 +138,7 @@ public static void mainHelper(final String[] args) new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.InvariantChecker", args, "", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/tools/ReadTrace.java b/java/daikon/tools/ReadTrace.java index a185839ebc..944e3b3609 100644 --- a/java/daikon/tools/ReadTrace.java +++ b/java/daikon/tools/ReadTrace.java @@ -62,7 +62,7 @@ public static void main(String[] args) { * @param args command-line arguments, like those of {@link #main} */ public static void mainHelper(String[] args) { - String[] traceFiles = DaikonGetopt.nonOptionArgs("daikon.tools.ReadTrace", args, usage); + String[] traceFiles = DaikonGetopt.nonOptionArgs(args, usage); if (traceFiles.length == 0) { throw new Daikon.UserError("No trace files specified" + Daikon.lineSep + usage); } diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 5778c4d294..f09969b1a4 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -60,15 +60,12 @@ private TraceSelect() { // Always set to non-null by mainHelper private static String @MonotonicNonNull [] sampleNames; - /** The name of this program, for use in messages. */ - private static final String progname = "daikon.tools.TraceSelect"; - /** The usage message for this program. */ private static final String usage = StringsPlume.joinLines( "USAGE: TraceSelect num_reps sample_size [options] [Daikon-args]...", "The options are -SEED n, -NOCLEAN, -INCLUDE_UNRETURNED, and -DO_DIFFS.", - "The Daikon-args start with the first .dtrace or .decls file.", + "The Daikon-args start with the first .dtrace or .decls file or other switch.", "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED -SEED 1000 foo.dtrace" + " foo2.dtrace foo.decls RatPoly.decls foo3.dtrace"); @@ -93,7 +90,7 @@ public static void main(String[] args) { */ public static void mainHelper(String[] args) { // Handles -h and --help before num_reps. - args = DaikonGetopt.argsAfterLeadingOptions(progname, args, usage); + args = DaikonGetopt.argsAfterLeadingOptions(args, usage); argles = args; if (args.length < 2) { throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); @@ -150,13 +147,15 @@ else if (args[i].toUpperCase(Locale.ENGLISH).equals("-DO_DIFFS")) { daikonArgStart = i + 1; } - // TODO: The current implementation assumes that a decls - // or dtrace file will be the first of the Daikon arguments, - // marking the end of the TraceSelect arguments. That is - // not necessarily true, especially in cases when someone - // uses a Daikon argument such as "--nohierarchy" or "--format java" - // and the manual examples place the arguments before any dtrace - // or decls arguments. + // The Daikon arguments start with the first .dtrace or .decls file, or with the first + // switch that is not a TraceSelect switch, such as "--nohierarchy" or "--format java". + + // -h and --help before the Daikon arguments print the usage message. + else if (!knowArgStart + && (args[i].equals("-h") || args[i].equals("--" + daikon.Daikon.help_SWITCH))) { + System.out.println(usage); + throw new daikon.Daikon.NormalTermination(); + } // For now, only the first dtrace file will be sampled else if (args[i].endsWith(".dtrace")) { @@ -177,14 +176,10 @@ else if (args[i].endsWith(".dtrace")) { } } - // Any other switch before the Daikon arguments is a request for help or an error. + // Any other switch starts the Daikon arguments. else if (!knowArgStart && args[i].startsWith("-")) { - if (args[i].startsWith("--") || args[i].length() == 2) { - // Prints the usage message for -h and --help, and describes any other such option. - DaikonGetopt.nonOptionArgs(progname, new String[] {args[i]}, usage); - } - throw new daikon.Daikon.UserError( - "Unrecognized command-line option " + args[i] + "; run with -h for usage"); + daikonArgStart = i; + knowArgStart = true; } } diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index e5c0073194..371d26ea25 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -675,8 +675,7 @@ public static void mainHelper(final String[] args) extra_assumptions = new LinkedHashMap<>(); - DaikonGetopt g = - new DaikonGetopt("daikon.tools.compare.LogicalCompare", args, "", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; boolean user_filters = false; while ((c = g.getopt()) != -1) { diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 485427045c..6bd1517d08 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -24,8 +24,7 @@ * *

The first argument is a Daikon {@code .inv} file -- a serialized file of Invariant objects. * All subsequent arguments are {@code Foo.java} files that are rewritten into {@code - * Foo.java-jmlannotated} versions; alternately, use the {@code -r} flag to process every {@code - * .java} file under the current directory. + * Foo.java-jmlannotated} versions. */ public class Annotate { @@ -138,7 +137,7 @@ public static void mainHelper(final String[] args) throws Exception { new LongOpt(max_invariants_pp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(no_reflection_SWITCH, LongOpt.NO_ARGUMENT, null, 0) }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.Annotate", args, "is", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "is", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index a6ddec8cac..76d4b23b05 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -87,7 +87,7 @@ public static void mainHelper(final String[] args) throws IOException { new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - DaikonGetopt g = new DaikonGetopt("daikon.tools.jtb.CreateSpinfo", args, "o:", longopts, usage); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { diff --git a/java/daikon/tools/jtb/CreateSpinfoC.java b/java/daikon/tools/jtb/CreateSpinfoC.java index 51c0f1a147..9fbb6babaa 100644 --- a/java/daikon/tools/jtb/CreateSpinfoC.java +++ b/java/daikon/tools/jtb/CreateSpinfoC.java @@ -47,15 +47,20 @@ public static void main(String[] args) { * @param args command-line arguments, like those of {@link #main} */ public static void mainHelper(String[] args) { - String[] files = DaikonGetopt.nonOptionArgs("daikon.tools.jtb.CreateSpinfoC", args, usage); + String[] files = DaikonGetopt.nonOptionArgs(args, usage); if (files.length != 1) { throw new Daikon.UserError(usage); } String inputFile = files[0]; + int dotPos = inputFile.lastIndexOf('.'); + if (dotPos <= inputFile.lastIndexOf(File.separatorChar)) { + throw new Daikon.UserError( + "File name has no extension: " + inputFile + Daikon.lineSep + usage); + } + String fileName = inputFile.substring(0, dotPos); System.out.println("Create spinfo file from file " + inputFile + " . . ."); + File temp = new File(fileName + ".temp"); try { - String fileName = inputFile.substring(0, inputFile.lastIndexOf('.')); - File temp = new File(fileName + ".temp"); // filter out the '\f' characters in the file try (Reader reader = Files.newBufferedReader(Paths.get(inputFile), UTF_8); Writer writer = Files.newBufferedWriter(temp.toPath(), UTF_8)) { @@ -66,35 +71,35 @@ public static void mainHelper(String[] args) { } } } catch (IOException e) { - temp.delete(); throw new Daikon.UserError(e, "Problem copying " + inputFile + " to " + temp); } + TranslationUnit root; try (FileInputStream fis = new FileInputStream(temp)) { @SuppressWarnings("UnusedVariable") // sets static variables for TranslationUnit() CParser parser = new CParser(fis); + // The parser reads lazily, so the stream must be open while it parses. + root = CParser.TranslationUnit(); } catch (IOException e) { throw new UncheckedIOException("problem reading " + temp, e); + } catch (ParseException e) { + throw new Daikon.UserError(e, "CreateSpinfoC encountered errors during parse"); } - TranslationUnit root = CParser.TranslationUnit(); StringFinder finder = new StringFinder(); - temp.delete(); root.accept(finder); - ConditionPrinter printer; + String spinfoFile = fileName + ".spinfo"; try { - printer = new ConditionPrinter(fileName + ".spinfo"); + ConditionPrinter printer = new ConditionPrinter(spinfoFile); printer.setActualStrings(finder.functionStringMapping); printer.setStringArrays(finder.stringMatrices); root.accept(printer); printer.close(); } catch (IOException e) { - System.out.println("File IO Error"); - System.out.println(e.getMessage()); + throw new Daikon.UserError(e, "Problem writing " + spinfoFile); } System.out.println("CreateSpinfoC: C program parsed successfully."); - } catch (ParseException e) { - System.out.println("CreateSpinfoC encountered errors during parse."); - System.out.println(e.getMessage()); + } finally { + temp.delete(); } } } diff --git a/java/daikon/tools/runtimechecker/CommandHandler.java b/java/daikon/tools/runtimechecker/CommandHandler.java index 3e733d6219..8cea732e7c 100644 --- a/java/daikon/tools/runtimechecker/CommandHandler.java +++ b/java/daikon/tools/runtimechecker/CommandHandler.java @@ -3,6 +3,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -28,6 +29,17 @@ public void usageMessage() { usageMessage(System.err); } + /** + * Returns the usage message. + * + * @return the usage message + */ + public String usageMessageString() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + usageMessage(new PrintStream(bytes, true, UTF_8)); + return bytes.toString(UTF_8).stripTrailing(); + } + /** * Prints the usage message. * diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index a4162b7497..39f5a375a9 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -16,6 +16,7 @@ import java.io.Writer; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Logger; import jtb.syntaxtree.*; @@ -81,13 +82,13 @@ public boolean handle(String[] args) { return false; } - String[] realArgs = new String[args.length - 1]; - for (int i = 0; i < realArgs.length; i++) { - realArgs[i] = args[i + 1]; - } + String[] realArgs = Arrays.copyOfRange(args, 1, args.length); Arguments arguments; try { arguments = readArguments(realArgs); + } catch (Daikon.NormalTermination e) { + // The user requested the usage message, which has been printed. + return true; } catch (Daikon.UserError e) { System.err.println(e.getMessage()); return false; @@ -95,9 +96,6 @@ public boolean handle(String[] args) { if (arguments == errorWhileReadingArguments) { return false; } - if (arguments == helpRequested) { - return true; - } // Set up debug traces; note this comes after reading command line options. daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); @@ -210,14 +208,10 @@ private static class Arguments { private static Arguments errorWhileReadingArguments = new Arguments("error while reading arguments", new ArrayList()); - /** Returned by {@link #readArguments} when the user requests the usage message. */ - private static Arguments helpRequested = new Arguments("help requested", new ArrayList()); - private Arguments readArguments(String[] args) { LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h'), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(output_only_high_conf_invariants_SWITCH, LongOpt.NO_ARGUMENT, null, 0), @@ -226,8 +220,7 @@ private Arguments readArguments(String[] args) { new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0) }; - DaikonGetopt g = - new DaikonGetopt("daikon.tools.runtimechecker.InstrumentHandler", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, this::usageMessageString); // The caller prints the usage message when this command fails. g.setUsageHint(null); int c; @@ -255,9 +248,6 @@ private Arguments readArguments(String[] args) { throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - usageMessage(System.out); - return helpRequested; default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index d6f7f20f8e..45e4c142e1 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -55,9 +55,7 @@ public void nonStaticMain(String[] args) { String[] commandAndArgs; try { - commandAndArgs = - DaikonGetopt.argsAfterLeadingOptions( - "daikon.tools.runtimechecker.Main", args, usageMessage(handlers)); + commandAndArgs = DaikonGetopt.argsAfterLeadingOptions(args, () -> usageMessage(handlers)); } catch (Daikon.DaikonTerminationException e) { Daikon.handleDaikonTerminationException(e); return; diff --git a/java/daikon/tools/runtimechecker/WriteViolationFile.java b/java/daikon/tools/runtimechecker/WriteViolationFile.java index 4f5070c828..ee16a07420 100644 --- a/java/daikon/tools/runtimechecker/WriteViolationFile.java +++ b/java/daikon/tools/runtimechecker/WriteViolationFile.java @@ -46,9 +46,7 @@ public static void usage() { public static void main(String[] args) { String[] classAndArgs; try { - classAndArgs = - DaikonGetopt.argsAfterLeadingOptions( - "daikon.tools.runtimechecker.WriteViolationFile", args, usageMessage); + classAndArgs = DaikonGetopt.argsAfterLeadingOptions(args, usageMessage); } catch (Daikon.DaikonTerminationException e) { Daikon.handleDaikonTerminationException(e); return; From 59befad4485b70349e8c62a009be87fcad26f0fd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 16:34:42 -0700 Subject: [PATCH 13/15] Address code review: TraceSelect args, CreateSpinfoC, runtimechecker, DaikonGetopt - TraceSelect: collect Daikon args during parsing; TraceSelect switches are never forwarded and no longer cause earlier Daikon args to be dropped. - CreateSpinfoC: treat '/' as a separator on Windows when finding the extension. - CommandHandler: a missing .doc resource is a BugInDaikon. - runtimechecker Main: reuse usageMessageString(). - DaikonGetopt: check that optstring/longopts do not already contain h/help. Co-Authored-By: Claude Opus 5.5 --- java/daikon/DaikonGetopt.java | 22 +++++++++- java/daikon/tools/TraceSelect.java | 43 +++++-------------- java/daikon/tools/jtb/CreateSpinfoC.java | 5 ++- .../tools/runtimechecker/CommandHandler.java | 6 +-- java/daikon/tools/runtimechecker/Main.java | 15 +++---- 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 0f8b17cb05..db8dbb5ad2 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -67,18 +67,36 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String * @param usage computes the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { - super(PROGNAME, argv, optstring + "h", withHelp(longopts)); + super(PROGNAME, argv, withH(optstring), withHelp(longopts)); this.usage = usage; opterr = false; } + /** + * Returns the given short options plus {@code -h}. + * + * @param optstring short options, in the format of {@link Getopt}; must not contain {@code h} + * @return {@code optstring} plus {@code -h} + */ + private static String withH(String optstring) { + if (optstring.indexOf('h') != -1) { + throw new IllegalArgumentException("optstring already contains h: " + optstring); + } + return optstring + "h"; + } + /** * Returns the given long options plus {@code --help}. * - * @param longopts long options + * @param longopts long options; must not contain {@code help} * @return {@code longopts} plus {@code --help} */ private static LongOpt[] withHelp(LongOpt[] longopts) { + for (LongOpt longopt : longopts) { + if (longopt.getName().equals(Daikon.help_SWITCH)) { + throw new IllegalArgumentException("longopts already contains " + Daikon.help_SWITCH); + } + } return ArraysPlume.append( longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index f09969b1a4..0c6832f9e8 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -16,7 +16,6 @@ import java.util.Random; import java.util.StringTokenizer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.dataflow.qual.Pure; import org.plumelib.util.FilesPlume; import org.plumelib.util.MultiRandSelector; @@ -41,10 +40,6 @@ private TraceSelect() { private static @MonotonicNonNull String fileName = null; - // Just a quick command line cache - // ... but I think it would it be better to pass args to invokeDaikon - // rather than introducing this variable. - private static String @MonotonicNonNull [] argles; // // stores the invocations in Strings // private static ArrayList invokeBuffer; @@ -53,7 +48,8 @@ private TraceSelect() { // always set to non-null by mainHelper private static @MonotonicNonNull Random randObj; - private static int daikonArgStart = 0; + /** The arguments to pass to Daikon, other than the .dtrace file. */ + private static List daikonArgs = new ArrayList<>(); // This allows us to simply call MultiDiff // with the same files we just created. @@ -91,7 +87,7 @@ public static void main(String[] args) { public static void mainHelper(String[] args) { // Handles -h and --help before num_reps. args = DaikonGetopt.argsAfterLeadingOptions(args, usage); - argles = args; + daikonArgs = new ArrayList<>(); if (args.length < 2) { throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); } @@ -119,14 +115,12 @@ public static void mainHelper(String[] args) { } catch (NumberFormatException e) { throw new daikon.Daikon.UserError("-SEED requires an integer argument, not " + seed); } - daikonArgStart = i + 1; } // NOCLEAN argument will leave the trace samples even after // the invariants from these samples have been generated else if (args[i].toUpperCase(Locale.ENGLISH).equals("-NOCLEAN")) { CLEAN = false; - daikonArgStart = i + 1; } // INCLUDE_UNRETURNED option will allow selecting method invocations @@ -134,7 +128,6 @@ else if (args[i].toUpperCase(Locale.ENGLISH).equals("-NOCLEAN")) { // either from a thrown Exception or abnormal termination. else if (args[i].toUpperCase(Locale.ENGLISH).equals("-INCLUDE_UNRETURNED")) { INCLUDE_UNRETURNED = true; - daikonArgStart = i + 1; } // DO_DIFFS will create a spinfo file for generating @@ -144,7 +137,6 @@ else if (args[i].toUpperCase(Locale.ENGLISH).equals("-INCLUDE_UNRETURNED")) { // samples. else if (args[i].toUpperCase(Locale.ENGLISH).equals("-DO_DIFFS")) { DO_DIFFS = true; - daikonArgStart = i + 1; } // The Daikon arguments start with the first .dtrace or .decls file, or with the first @@ -165,22 +157,16 @@ else if (args[i].endsWith(".dtrace")) { throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); } - if (!knowArgStart) { - daikonArgStart = i; + knowArgStart = true; + } else { + // A .decls file or any other switch starts the Daikon arguments. + if (args[i].endsWith(".decls") || args[i].startsWith("-")) { knowArgStart = true; } - } else if (args[i].endsWith(".decls")) { - if (!knowArgStart) { - daikonArgStart = i; - knowArgStart = true; + if (knowArgStart) { + daikonArgs.add(args[i]); } } - - // Any other switch starts the Daikon arguments. - else if (!knowArgStart && args[i].startsWith("-")) { - daikonArgStart = i; - knowArgStart = true; - } } // if no seed provided, use default Random() constructor @@ -290,7 +276,6 @@ private static void deleteQuietly(String fileName) { } } - @RequiresNonNull("argles") private static void invokeDaikon(String dtraceName) throws IOException { System.out.println("Created file: " + dtraceName); @@ -301,14 +286,8 @@ private static void invokeDaikon(String dtraceName) throws IOException { daikonArgsList.add("-o"); daikonArgsList.add(dtraceName + ".inv"); - // find all the Daikon args except for the original - // single dtrace file. - for (int i = daikonArgStart; i < argles.length; i++) { - if (argles[i].endsWith(".dtrace")) { - continue; - } - daikonArgsList.add(argles[i]); - } + // all the Daikon args except for the original single dtrace file + daikonArgsList.addAll(daikonArgs); // create an array to store the Strings in daikonArgsList String[] daikonArgs = daikonArgsList.toArray(new String[0]); diff --git a/java/daikon/tools/jtb/CreateSpinfoC.java b/java/daikon/tools/jtb/CreateSpinfoC.java index 9fbb6babaa..391ae18ffd 100644 --- a/java/daikon/tools/jtb/CreateSpinfoC.java +++ b/java/daikon/tools/jtb/CreateSpinfoC.java @@ -53,7 +53,10 @@ public static void mainHelper(String[] args) { } String inputFile = files[0]; int dotPos = inputFile.lastIndexOf('.'); - if (dotPos <= inputFile.lastIndexOf(File.separatorChar)) { + // On Windows, both '/' and '\\' are separators. + int separatorPos = + Math.max(inputFile.lastIndexOf('/'), inputFile.lastIndexOf(File.separatorChar)); + if (dotPos <= separatorPos) { throw new Daikon.UserError( "File name has no extension: " + inputFile + Daikon.lineSep + usage); } diff --git a/java/daikon/tools/runtimechecker/CommandHandler.java b/java/daikon/tools/runtimechecker/CommandHandler.java index 8cea732e7c..f54877aa81 100644 --- a/java/daikon/tools/runtimechecker/CommandHandler.java +++ b/java/daikon/tools/runtimechecker/CommandHandler.java @@ -2,6 +2,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import daikon.Daikon; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -44,6 +45,7 @@ public String usageMessageString() { * Prints the usage message. * * @param out where to print the usage message + * @throws Daikon.BugInDaikon if the documentation file is missing */ public void usageMessage(PrintStream out) { String[] classnameArray = getClass().getName().split("\\."); @@ -52,9 +54,7 @@ public void usageMessage(PrintStream out) { String docFile = simpleClassname + ".doc"; InputStream in = getClass().getResourceAsStream(docFile); if (in == null) { - // This is an error message, so it goes to standard error even if `out` is standard output. - System.err.println("Didn't find documentation " + docFile + " for " + getClass()); - return; + throw new Daikon.BugInDaikon("Didn't find documentation " + docFile + " for " + getClass()); } try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { String line; diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 45e4c142e1..bfebc5889a 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,14 +1,12 @@ package daikon.tools.runtimechecker; -import static java.nio.charset.StandardCharsets.UTF_8; - import daikon.Daikon; import daikon.DaikonGetopt; -import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.StringJoiner; /** * Main entrypoint for the instrumenter. Passes control to whichever handler can handle the @@ -35,11 +33,12 @@ protected void usageMessage(List handlers, PrintStream out) { * @return the usage message of this and of each handler */ private String usageMessage(List handlers) { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PrintStream out = new PrintStream(bytes, true, UTF_8); - usageMessage(out); - usageMessage(handlers, out); - return bytes.toString(UTF_8).stripTrailing(); + StringJoiner result = new StringJoiner(Daikon.lineSep); + result.add(usageMessageString()); + for (CommandHandler h : handlers) { + result.add(h.usageMessageString()); + } + return result.toString(); } /** From cdc742c23d34725d8a901266bd794fefc337f524 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 16:56:29 -0700 Subject: [PATCH 14/15] Address code review: TraceSelect state and args, runtimechecker usage, DaikonGetopt argv copy - TraceSelect.mainHelper resets all static state on each call. - TraceSelect passes every non-TraceSelect argument to Daikon rather than silently dropping arguments before the first .dtrace/.decls/switch. - runtimechecker Main: a failure printing usage no longer prevents exit; remove unused usageMessage(List, PrintStream). - DaikonGetopt does not permute the caller's argv. - Annotate usage documents --help. Co-Authored-By: Claude Opus 5.5 --- java/daikon/DaikonGetopt.java | 6 ++-- java/daikon/tools/TraceSelect.java | 39 ++++++++++++---------- java/daikon/tools/jtb/Annotate.java | 2 +- java/daikon/tools/runtimechecker/Main.java | 20 ++++------- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index db8dbb5ad2..0f71e31406 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -160,13 +160,15 @@ public static String[] argsAfterLeadingOptions(String[] argv, Supplier u */ private static String[] argsAfterOptions( String[] argv, String optstring, Supplier usage) { - DaikonGetopt g = new DaikonGetopt(argv, optstring, new LongOpt[0], 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(argv, g.getOptind(), argv.length); + return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); } /** diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 0c6832f9e8..7bf9c29bd3 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -61,7 +61,7 @@ private TraceSelect() { StringsPlume.joinLines( "USAGE: TraceSelect num_reps sample_size [options] [Daikon-args]...", "The options are -SEED n, -NOCLEAN, -INCLUDE_UNRETURNED, and -DO_DIFFS.", - "The Daikon-args start with the first .dtrace or .decls file or other switch.", + "The Daikon-args start with the first argument that is not one of those options.", "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED -SEED 1000 foo.dtrace" + " foo2.dtrace foo.decls RatPoly.decls foo3.dtrace"); @@ -87,7 +87,14 @@ public static void main(String[] args) { public static void mainHelper(String[] args) { // Handles -h and --help before num_reps. args = DaikonGetopt.argsAfterLeadingOptions(args, usage); + // Reset the state from any previous call. + CLEAN = true; + INCLUDE_UNRETURNED = false; + DO_DIFFS = false; daikonArgs = new ArrayList<>(); + String inputFile = null; + Random rand = null; + if (args.length < 2) { throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); } @@ -111,7 +118,7 @@ public static void mainHelper(String[] args) { } String seed = args[++i]; try { - randObj = new Random(Long.parseLong(seed)); + rand = new Random(Long.parseLong(seed)); } catch (NumberFormatException e) { throw new daikon.Daikon.UserError("-SEED requires an integer argument, not " + seed); } @@ -139,8 +146,8 @@ else if (args[i].toUpperCase(Locale.ENGLISH).equals("-DO_DIFFS")) { DO_DIFFS = true; } - // The Daikon arguments start with the first .dtrace or .decls file, or with the first - // switch that is not a TraceSelect switch, such as "--nohierarchy" or "--format java". + // The Daikon arguments start with the first argument that is not a TraceSelect switch, + // such as a .dtrace or .decls file, or "--nohierarchy" or "--format java". // -h and --help before the Daikon arguments print the usage message. else if (!knowArgStart @@ -151,35 +158,31 @@ else if (!knowArgStart // For now, only the first dtrace file will be sampled else if (args[i].endsWith(".dtrace")) { - if (fileName == null) { - fileName = args[i]; + if (inputFile == null) { + inputFile = args[i]; } else { throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); } knowArgStart = true; } else { - // A .decls file or any other switch starts the Daikon arguments. - if (args[i].endsWith(".decls") || args[i].startsWith("-")) { - knowArgStart = true; - } - if (knowArgStart) { - daikonArgs.add(args[i]); - } + knowArgStart = true; + daikonArgs.add(args[i]); } } // if no seed provided, use default Random() constructor - if (randObj == null) { - randObj = new Random(); - } + randObj = (rand != null) ? rand : new Random(); sampleNames = new String[num_reps + 1]; sampleNames[0] = "-p"; - if (fileName == null) { - throw new daikon.Daikon.UserError("No .dtrace file name specified"); + if (inputFile == null) { + throw new daikon.Daikon.UserError( + "No .dtrace file name specified (the trace file must be uncompressed and its name must" + + " end with \".dtrace\")"); } + fileName = inputFile; try { diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 6bd1517d08..cee7292928 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -87,7 +87,7 @@ private Annotate() { private static String usage = StringsPlume.joinLines( "Usage: java daikon.tools.Annotate FILE.inv FILE.java ...", - " -h Display this usage message", + " -h, --help Display this usage message", " -i Insert invariants not supported by ESC with \"!\" instead of \"@\";", " by default these \"inexpressible\" invariants are simply omitted", " -s Use // comments rather than /* comments", diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index bfebc5889a..188b747947 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -2,7 +2,6 @@ import daikon.Daikon; import daikon.DaikonGetopt; -import java.io.PrintStream; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -14,18 +13,6 @@ */ public class Main extends CommandHandler { - /** - * Prints the usage message of each handler. - * - * @param handlers the handlers whose usage messages to print - * @param out where to print the usage messages - */ - protected void usageMessage(List handlers, PrintStream out) { - for (CommandHandler h : handlers) { - h.usageMessage(out); - } - } - /** * Returns the usage message of this and of each handler. * @@ -101,7 +88,12 @@ public void nonStaticMain(String[] args) { System.err.println( "For more help, invoke the instrumenter with \"help\" as its sole argument."); } else { - h.usageMessage(); + // Don't let a problem printing the usage message prevent the exit below. + try { + h.usageMessage(); + } catch (RuntimeException e) { + System.err.println(e.getMessage()); + } } System.exit(1); } else { From 499ce004eabdd0ba88a772aefc5b02f619e67053 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 17:56:18 -0700 Subject: [PATCH 15/15] Address code review: AnnotateNullable usage to stderr on error; remove stale DtraceDiff comment --- java/daikon/AnnotateNullable.java | 2 +- java/daikon/tools/DtraceDiff.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/java/daikon/AnnotateNullable.java b/java/daikon/AnnotateNullable.java index 957fe44e40..17769b8dfd 100644 --- a/java/daikon/AnnotateNullable.java +++ b/java/daikon/AnnotateNullable.java @@ -79,7 +79,7 @@ public static void main(String[] args) throws IOException { } if (inv_files.length != 1) { System.err.println("must specify exactly one invariant file"); - options.printUsage(); + options.printUsage(System.err); System.exit(1); } diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index e1c7bbce3f..0fe001621e 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -225,7 +225,6 @@ public static void mainHelper(final String[] args) { throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } - // short options default: throw new Daikon.BugInDaikon("getopt() returned " + c); }