diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 0d2fb61b9b..812178cbc6 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -5,6 +5,8 @@ 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; /** @@ -20,8 +22,11 @@ public class DaikonGetopt extends Getopt { */ private static final String PROGNAME = "DaikonGetopt"; - /** The usage message printed for {@code -h} and {@code --help}. */ - private final String usage; + /** Computes the usage message printed for {@code -h} and {@code --help}. */ + private final 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"; /** * Creates a command-line option processor that recognizes short and long options, plus {@code -h} @@ -33,6 +38,20 @@ public class DaikonGetopt extends Getopt { * @param usage the usage message to print for {@code -h} and {@code --help} */ 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, @@ -43,6 +62,49 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String opterr = false; } + /** + * 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 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[] 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(argv, "+", usage); + } + + /** + * 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; + } + /** * Processes the command-line arguments of a program that takes no options other than {@code -h} * and {@code --help}. @@ -54,7 +116,7 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String * @throws Daikon.UserError if an argument is any other option */ public static String[] nonOptionArgs(String[] argv, String usage) { - return argsAfterOptions(argv, "", usage); + return argsAfterOptions(argv, "", () -> usage); } /** @@ -63,12 +125,13 @@ public static String[] nonOptionArgs(String[] argv, String usage) { * * @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[] argv, String optstring, String usage) { + private static String[] argsAfterOptions( + String[] argv, String optstring, Supplier 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); @@ -93,10 +156,14 @@ private static String[] argsAfterOptions(String[] argv, String optstring, String public int getopt() { int c = super.getopt(); if (c == '?' || c == ':') { - throw new Daikon.UserError(badOptionMessage() + "; run with -h for usage"); + String message = badOptionMessage(); + if (usageHint != null) { + message += "; " + usageHint; + } + throw new Daikon.UserError(message); } if (c == 'h') { - System.out.println(usage); + System.out.println(usage.get()); throw new Daikon.NormalTermination(); } return c; diff --git a/java/daikon/tools/runtimechecker/CommandHandler.java b/java/daikon/tools/runtimechecker/CommandHandler.java index 7097f6ac9d..31c17e44b5 100644 --- a/java/daikon/tools/runtimechecker/CommandHandler.java +++ b/java/daikon/tools/runtimechecker/CommandHandler.java @@ -2,11 +2,13 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import daikon.Daikon; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; +import java.util.stream.Collectors; /** * A command handler handles a set of commands. A command is the first argument given to the @@ -18,25 +20,41 @@ public boolean handles(String command) { throw new UnsupportedOperationException(); } - public boolean handle(String[] args) { + /** + * Performs the command. + * + * @param args the command, followed by its arguments + * @throws Daikon.DaikonTerminationException if the program should exit + */ + public void handle(String[] args) { throw new UnsupportedOperationException(); } - public void usageMessage() { + /** + * Returns the usage message, which is the contents of this class's documentation file. + * + * @return the usage message + * @throws Daikon.BugInDaikon if the documentation file is missing + */ + public String usageMessageString() { 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()); - return; + throw new Daikon.BugInDaikon("Didn't find documentation " + docFile + " for " + getClass()); } + // When Daikon requires Java 11 or later, simplify this to: + // try (in) { + // return new String(in.readAllBytes(), UTF_8).stripTrailing(); + // } + // and remove the imports of BufferedReader, InputStreamReader, and Collectors. try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - System.err.println(line); - } + return reader + .lines() + .collect(Collectors.joining(System.lineSeparator())) + .replaceAll("\\s+$", ""); } catch (IOException e) { throw new UncheckedIOException("problem reading " + docFile, e); } 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 ffd4e77a48..3dd1510198 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -5,23 +5,23 @@ 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; 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.*; import jtb.visitor.TreeDumper; import jtb.visitor.TreeFormatter; -import org.checkerframework.checker.interning.qual.UsesObjectEquals; /** * Instruments a file to check invariant violations at run time. Violated invariants are stored in a @@ -71,24 +71,13 @@ public boolean handles(String command) { // oneFile.roots) {" ) @Override - public boolean handle(String[] args) { + public void handle(String[] args) { Daikon.output_format = daikon.inv.OutputFormat.JAVA; daikon.PrintInvariants.wrap_xml = true; - if (!args[0].equals("instrument")) { - System.err.println("Command (first argument) to instrumenter was not recognized."); - 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 = readArguments(realArgs); - if (arguments == errorWhileReadingArguments) { - return false; - } // Set up debug traces; note this comes after reading command line options. daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); @@ -102,10 +91,7 @@ public boolean handle(String[] args) { ppts = FileIO.read_serialized_pptmap(new File(arguments.invFile), true /* use saved config */); } catch (IOException e) { - System.err.println("Exception while reading invariant file " + arguments.invFile); - System.err.println(e.getMessage()); - e.printStackTrace(); - return false; + throw new Daikon.UserError(e, "Problem while reading invariant file " + arguments.invFile); } // Compile original sources (because daikon.tools.jtb.Ast accesses @@ -177,17 +163,11 @@ public boolean handle(String[] args) { } } catch (IOException e) { - System.err.println("Exception while instrumenting " + oneFile.fileName); - System.err.println(e.getMessage()); - e.printStackTrace(); - return false; + throw new Daikon.UserError(e, "Problem while writing output for " + oneFile.fileName); } } - - return true; } - @UsesObjectEquals private static class Arguments { String invFile; List javaFileNames; @@ -198,9 +178,17 @@ private static class Arguments { } } - private static Arguments errorWhileReadingArguments = - new Arguments("error while reading arguments", new ArrayList()); - + /** Text appended to the description of a bad command-line argument. */ + private static final String usageHint = "run with \"instrument -h\" for usage"; + + /** + * Parses the command-line arguments of the {@code instrument} command. + * + * @param args the arguments that follow the command name + * @return the parsed arguments + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is malformed or missing + */ private Arguments readArguments(String[] args) { LongOpt[] longopts = @@ -213,7 +201,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(args, "", longopts, this::usageMessageString); + g.setUsageHint(usageHint); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -236,37 +225,33 @@ 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); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; default: - System.out.println("unrecognized option" + c); - return errorWhileReadingArguments; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } // The index of the first non-option argument -- the name of the // invariant file. int argindex = g.getOptind(); if (argindex >= args.length) { - System.out.println("Error: No .inv file or .java file arguments supplied."); - return errorWhileReadingArguments; + throw new Daikon.UserError("No .inv file or .java file arguments supplied; " + usageHint); } String invfile = args[argindex]; argindex++; if (!(invfile.endsWith(".inv") || invfile.endsWith(".inv.gz"))) { - System.out.println("Error: first argument must be a file ending in .inv or .inv.gz."); - return errorWhileReadingArguments; + throw new Daikon.UserError( + "First argument must be a file ending in .inv or .inv.gz; " + usageHint); } if (argindex >= args.length) { - System.out.println("Error: No .java file arguments supplied."); - return errorWhileReadingArguments; + throw new Daikon.UserError("No .java file arguments supplied; " + usageHint); } List javaFileNames = new ArrayList<>(); for (; argindex < args.length; argindex++) { String javafile = args[argindex]; if (!javafile.endsWith(".java")) { - System.out.println("File does not end in .java: " + javafile); - return errorWhileReadingArguments; + throw new Daikon.UserError("File does not end in .java: " + javafile + "; " + usageHint); } javaFileNames.add(javafile); } diff --git a/java/daikon/tools/runtimechecker/Main.doc b/java/daikon/tools/runtimechecker/Main.doc index 5ddee5b1f5..b3814203a9 100644 --- a/java/daikon/tools/runtimechecker/Main.doc +++ b/java/daikon/tools/runtimechecker/Main.doc @@ -6,5 +6,5 @@ invariant properties are checked at run time. The instrumenter can operate in the following modes, indicated by the first argument to the daikon.tools.runtimecheker.Main class: -help +help, -h, --help Print a usage message diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 9259e07f45..7404ef6ba1 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,8 +1,11 @@ package daikon.tools.runtimechecker; +import daikon.Daikon; +import daikon.DaikonGetopt; 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 @@ -10,75 +13,68 @@ */ public class Main extends CommandHandler { - protected void usageMessage(List handlers) { + /** + * 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 fullUsageMessage(List handlers) { + StringJoiner result = new StringJoiner(Daikon.lineSep); + result.add(usageMessageString()); for (CommandHandler h : handlers) { - h.usageMessage(); + result.add(h.usageMessageString()); } + return result.toString(); } + /** The hint printed after an error message. */ + private static final String helpHint = "For more help, run the instrumenter with -h."; + /** * Entry point for the instrumenter. Passes control to whichever handler can handle the * user-specified command. * * @param args the arguments to the program + * @throws Daikon.DaikonTerminationException if the instrumenter should exit */ public void nonStaticMain(String[] args) { List handlers = Collections.singletonList((CommandHandler) new InstrumentHandler()); - if (args.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); + String[] commandAndArgs = + DaikonGetopt.argsAfterLeadingOptions(args, () -> fullUsageMessage(handlers)); + if (commandAndArgs.length < 1) { + throw new Daikon.UserError("No command given." + Daikon.lineSep + helpHint); } - if (args[0].toUpperCase(Locale.ENGLISH).equals("HELP") || args[0].equals("?")) { - usageMessage(); - usageMessage(handlers); - System.exit(0); + if (commandAndArgs[0].toUpperCase(Locale.ENGLISH).equals("HELP") + || commandAndArgs[0].equals("?")) { + System.out.println(fullUsageMessage(handlers)); + throw new Daikon.NormalTermination(); } - String command = args[0]; - - boolean success = false; + String command = commandAndArgs[0]; CommandHandler h = null; - try { - - for (CommandHandler handler : handlers) { - if (handler.handles(command)) { - h = handler; - success = h.handle(args); - if (!success) { - System.err.println("The command you issued returned a failing status flag."); - } - break; - } - } - - } catch (Throwable e) { - System.out.println("Throwable thrown while handling command:" + e); - e.printStackTrace(); - success = false; - } finally { - if (!success) { - System.err.println("The instrumenter failed."); - if (h == null) { - System.err.println("Unknown command: " + command); - System.err.println( - "For more help, invoke the instrumenter with \"help\" as its sole argument."); - } else { - h.usageMessage(); - } - System.exit(1); - } else { + for (CommandHandler handler : handlers) { + if (handler.handles(command)) { + h = handler; + break; } } + if (h == null) { + throw new Daikon.UserError("Unknown command: " + command + Daikon.lineSep + helpHint); + } + + h.handle(commandAndArgs); } public static void main(String[] args) { - Main main = new Main(); - main.nonStaticMain(args); + try { + new Main().nonStaticMain(args); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + } } } diff --git a/java/daikon/tools/runtimechecker/WriteViolationFile.java b/java/daikon/tools/runtimechecker/WriteViolationFile.java index dcfb548648..4d4c7b9ee8 100644 --- a/java/daikon/tools/runtimechecker/WriteViolationFile.java +++ b/java/daikon/tools/runtimechecker/WriteViolationFile.java @@ -2,6 +2,8 @@ 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; @@ -11,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 @@ -26,47 +30,64 @@ private WriteViolationFile() { throw new UnsupportedOperationException("Do not instantiate"); } - 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."); - } + /** 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."); - @SuppressWarnings("Finally") - public static void main(String[] args) { - if (args.length == 0) { - System.out.println("Error: no class specified"); - usage(); - System.exit(1); - } - @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]; - } + /** + * Returns the main method of the given class. + * + * @param className the binary name of a class + * @return the main method of the class + * @throws Daikon.UserError if the class does not exist or has no public static main method + */ + private static Method getMainMethod(String className) { Class cls; try { - cls = Class.forName(class_name); - } catch (Exception e) { - e.printStackTrace(); - throw new Error("Cannot find class " + class_name); + @SuppressWarnings("signature") // checked by Class.forName, which throws if it is malformed + @BinaryName String binaryName = className; + cls = Class.forName(binaryName); + } catch (ClassNotFoundException e) { + throw new Daikon.UserError(e, "Cannot find class " + className); } - Method main_method; + Method mainMethod; try { - main_method = cls.getMethod("main", new Class[] {String[].class}); - } catch (Exception e) { - throw new Error("Cannot find main method in class " + class_name); + mainMethod = cls.getMethod("main", new Class[] {String[].class}); + } catch (NoSuchMethodException e) { + throw new Daikon.UserError(e, "Cannot find public main method in class " + className); } - int mods = main_method.getModifiers(); - if (!Modifier.isPublic(mods)) { - throw new Error("main method is not public in class " + class_name); + if (!Modifier.isStatic(mainMethod.getModifiers())) { + throw new Daikon.UserError("main method is not static in class " + className); } - if (!Modifier.isStatic(mods)) { - throw new Error("main method is not static in class " + class_name); + return mainMethod; + } + + public static void main(String[] args) { + try { + mainHelper(args); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + } + } + + /** + * This does the work of {@link #main(String[])}, but it never calls System.exit. + * + * @param args the command-line arguments + * @throws Daikon.DaikonTerminationException if the arguments are malformed or request help + */ + @SuppressWarnings("Finally") + private static void mainHelper(String[] args) { + String[] classAndArgs = DaikonGetopt.argsAfterLeadingOptions(args, usageMessage); + if (classAndArgs.length == 0) { + throw new Daikon.UserError("No class specified" + Daikon.lineSep + usageMessage); } + Method main_method = getMainMethod(classAndArgs[0]); + String[] main_args = Arrays.copyOfRange(classAndArgs, 1, classAndArgs.length); // This will produce no output if the program under test calls // System.exit(1). Let's hope it doesn't (and fix this later).