package org.rascalmpl.library.experiments.Compiler.Commands; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Function; import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.RascalExecutionContext; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.value.IBool; import org.rascalmpl.value.IList; import org.rascalmpl.value.ISourceLocation; import org.rascalmpl.value.IString; import org.rascalmpl.value.IValue; import org.rascalmpl.value.exceptions.FactTypeUseException; import org.rascalmpl.value.io.StandardTextReader; import org.rascalmpl.value.type.Type; import org.rascalmpl.value.type.TypeFactory; import org.rascalmpl.value.type.TypeStore; import org.rascalmpl.values.ValueFactoryFactory; /** * Option values can have the foloowing types; * */ enum OptionType {INT, STR, BOOL, LOCS, LOC}; /** * Create CommandOptions for a main program. * * A command option is one of * <ul> * <li>boolean: --optionName * <li>string: --optionName stringValue * <li>loc: --optionName sourceLocationValue * <li>locs: --optionName sourceLocationValue * sourceLocationValue are either (quoted) Rascal source locations or file path names * multiple locs options of the same name accumulate to a list of source locations * </ul> * Command options define properties for the initialized and configuration of the command. * <p> * A moduleName is a qualified Rascal module name. A command can have 1 or more than one modules names. * <p> * A command looks like this: * <p> * commandName commandOptions* moduleName+ moduleOptions* * <p> * A module option looks the same as a command option but is associated with the module(s) given in the command. * CommandOptions provides a fluent interface to create options of the above type, e.g. intOption, boolOption, etc. * Each option is created by a sequence starting with the option creation and ending with the associated help message for that option. * In between, defaults can be set and omitted defaults have sensible values. * <p> *<code> * CommandOptions opts = new CommandOptions("commandName"); * <p> * opts.intOption("X").intDefault(42).help("X is a very good option") * <p> * .boolOption("Y").help("and Y too!") * <p> * .mdule("Module to analyze") * <p> * .handleArgs(args); // the string arguments coming from the command line * </code> */ public class CommandOptions { protected TypeFactory tf; protected org.rascalmpl.value.IValueFactory vf; private boolean inCommandOptions = true; private boolean singleModule = true; private IList modules; private int minModules = 0; private int maxModules = 0; private String moduleHelp; protected Options commandOptions; private Options moduleOptions; private String commandName; public CommandOptions(String commandName){ this.commandName = commandName; tf = TypeFactory.getInstance(); vf = ValueFactoryFactory.getValueFactory(); commandOptions = new Options(); moduleOptions = new Options(); modules = vf.list(); moduleHelp = "Rascal Module"; } public CommandOptions addOption(Option option){ (inCommandOptions ? commandOptions : moduleOptions).add(option); return this; } /****************************************************************************/ /* Bool options */ /****************************************************************************/ /** * Declare a bool option (a single boolean value) * @param name of option * @return OptionBuilder */ public OptionBuilder boolOption(String name){ return new OptionBuilder(this, OptionType.BOOL, name); } /** * Get the value of a bool option from the command options * @param name * @return value of option */ public boolean getCommandBoolOption(String name){ return ((IBool) commandOptions.get(OptionType.BOOL, name)).getValue(); } /** * Get the value of a bool option from the module options * @param name * @return value of option */ public boolean getModuleBoolOption(String name){ return ((IBool) moduleOptions.get(OptionType.BOOL, name)).getValue(); } /****************************************************************************/ /* String options */ /****************************************************************************/ /** * Declare a string option (a single string value) * @param name of option * @return OptionBuilder */ public OptionBuilder strOption(String name){ return new OptionBuilder(this, OptionType.STR, name); } /** * Get the value of a string option from the command options * @param name * @return value of option */ public String getCommandStringOption(String name){ return ((IString) commandOptions.get(OptionType.STR, name)).getValue(); } /** * Get the value of a string option from the module options * @param name * @return value of option */ public String getModuleStringOption(String name){ return ((IString) moduleOptions.get(OptionType.STR, name)).getValue(); } /****************************************************************************/ /* Loc options */ /****************************************************************************/ /** * Declare a loc option (a single location) * @param name of option * @return OptionBuilder */ public OptionBuilder locOption(String name){ return new OptionBuilder(this, OptionType.LOC, name); } /** * Get the value of a loc option from the command options * @param name * @return value of option */ public ISourceLocation getCommandLocOption(String name){ return (ISourceLocation) commandOptions.get(OptionType.LOC, name); } /** * Get the value of a loc option from the module options * @param name * @return value of option */ public ISourceLocation getModuleLocOption(String name){ return (ISourceLocation) moduleOptions.get(OptionType.LOC, name); } /****************************************************************************/ /* Locs options */ /****************************************************************************/ /** * Declare a locs option (a list of locations) * @param name of option * @return OptionBuilder */ public OptionBuilder locsOption(String name){ return new OptionBuilder(this, OptionType.LOCS, name); } /** * Get the value of a locs option from the command options * @param name * @return value of option */ public IList getCommandLocsOption(String name){ return (IList) commandOptions.get(OptionType.LOCS, name); } /** * Get the value of a locs option from the module options * @param name * @return value of option */ public IList getModuleLocsOption(String name){ return (IList) moduleOptions.get(OptionType.LOCS, name); } /****************************************************************************/ /* Module argument(s) */ /****************************************************************************/ /** * Command has one Module (Rascal Module, Course, ...) as argument * @param helpText describes the role of the single module argument * @return this CommandOptions */ public CommandOptions module(String helpText){ return modules(helpText, 1, 1); } /** * Get the name of the single module argument of the command * @return module name */ public IString getModule(){ return (IString) modules.get(0); } /** * Command has one or more Modules as argument * @param helpText describes the role of the one or more module arguments * @return this CommandOptions */ public CommandOptions modules(String helpText){ return modules(helpText, 1, 1000000); } /** * Command has min or more Modules as argument * @param helpText describes the role of the one or more module arguments * @return this CommandOptions */ public CommandOptions modules(String helpText, int min){ return modules(helpText, min, 1000000); } /** * Command has between min and max Modules as argument * @param helpText describes the role of the one or more module arguments * @return this CommandOptions */ public CommandOptions modules(String helpText, int min, int max){ minModules = min; maxModules = max; inCommandOptions = false; moduleHelp = helpText; return this; } /** * Get the names of the module arguments of the command * @return list of module names */ public IList getModules(){ return modules; } /****************************************************************************/ /* Processing of all command line arguments */ /****************************************************************************/ private String getOptionValue(String[] args, int i){ if(i >= args.length - 1 || args[i + 1].startsWith("--")){ printUsageAndExit("Missing value for option " + args[i]); return ""; } return args[i + 1]; } /** * Handle command line options and create help and usage info * @param args a list of options and their values */ public void handleArgs(String[] args){ vf = ValueFactoryFactory.getValueFactory(); boolean mainSeen = false; Options currentOptions; int i = 0; while(i < args.length){ if(args[i].startsWith("--")){ String option = args[i].substring(2, args[i].length()); currentOptions = !mainSeen ? commandOptions : moduleOptions; if(currentOptions.contains(OptionType.BOOL, option)){ currentOptions.set(OptionType.BOOL, option, vf.bool(true)); i += 1; } else { if(currentOptions.contains(OptionType.STR, option)){ currentOptions.set(OptionType.STR, option, vf.string(getOptionValue(args, i))); } else if(currentOptions.contains(OptionType.LOCS, option)){ ISourceLocation newLoc = convertLoc(getOptionValue(args, i)); currentOptions.update(OptionType.LOCS, option, (current) -> current == null ? vf.list(newLoc) : ((IList) current).append(newLoc)); } else if(currentOptions.contains(OptionType.LOC, option)){ currentOptions.set(OptionType.LOC, option, convertLoc(getOptionValue(args, i))); } else { printUsageAndExit("Unknown command option " + args[i]); return; } i += 2; } } else { modules = modules.append(vf.string(args[i])); mainSeen = true; i++; } } String ndHelp = noDefaultsHelpText(); if(commandOptions.contains(OptionType.BOOL, "noDefaults")){ if(!ndHelp.isEmpty()){ for(Option option : commandOptions){ if(option.name.equals("noDefaults")){ option.helpText = ndHelp; } } } } else { if(!ndHelp.isEmpty()){ commandOptions.add(new Option(OptionType.BOOL, "noDefaults", vf.bool(false), null, false, ndHelp)); } } if(commandOptions.hasNonDefaultValue(OptionType.BOOL, "help")) { help(); System.exit(0); } checkDefaults(); if (modules.length() == 0 && minModules > 0) { printUsageAndExit("Missing Rascal module" + (singleModule ? "" : "s")); } else if (modules.length() > 0 && maxModules == 0) { printUsageAndExit("No modules expected"); } else if(modules.length() > maxModules) { printUsageAndExit("Too many modules defined: " + modules); } } /****************************************************************************/ /* (Utilities for) consistency checking */ /****************************************************************************/ private void checkDefaults(){ for(Option option : commandOptions){ option.checkDefault(this); } for(Option option : moduleOptions){ option.checkDefault(this); } } /****************************************************************************/ /* Usage and help generation */ /****************************************************************************/ /** * Print usage of the command and exit * * @param msg error message */ private void printUsageAndExit(String msg){ System.err.println(usage(msg));; System.exit(-1); } /** * @param msg error message * @return the usage string for the command */ private String usage(String msg){ StringBuffer w = new StringBuffer(); if(!msg.isEmpty()){ w.append(msg).append("\n"); } String prefix = "Usage: " + commandName; String indent = new String(new char[prefix.length()]).replace('\0', ' '); w.append(prefix); int nopt = 0; for(Option option : commandOptions){ nopt++; if(nopt == 10){ nopt = 0; w.append("\n").append(indent); } w.append(option.help()); } w.append("\n").append(indent).append(singleModule ? " <RascalModule>" : " <RascalModules>"); nopt = 0; for(Option option : moduleOptions){ nopt++; if(nopt == 10){ nopt = 0; w.append("\n").append(indent); } w.append(option.help()); } return w.toString(); } private String noDefaultsHelpText(){ List<String> respectNoDefaults = commandOptions.getAllRespectNoDefaults(); respectNoDefaults.addAll(moduleOptions.getAllRespectNoDefaults()); if(respectNoDefaults.isEmpty()){ return ""; } StringWriter w = new StringWriter(); w.append("Forbid use of default values for"); String sep = " "; for(String name : respectNoDefaults){ w.append(sep).append(name); sep = ", "; } return w.toString(); } private void help(){ System.err.println(usage("")); System.err.println(); for(Option option : commandOptions){ System.err.printf("%20s %s\n", option.help(), option.helpText); } System.err.printf("%20s %s\n", singleModule ? " <RascalModule>" : " <RascalModules>", moduleHelp); for(Option option : moduleOptions){ System.err.printf("%20s %s\n", option.help(), option.helpText); } System.exit(-1); } /****************************************************************************/ /* Convenience methods */ /****************************************************************************/ /** * @return all module options as a Java Map */ public Map<String,IValue> getModuleOptions(){ HashMap<String, IValue> mainOptionsMap = new HashMap<>(); for(Option option : moduleOptions){ mainOptionsMap.put(option.name, option.currentValue); } return mainOptionsMap; } /** * @return all module options as a Map */ public Map<String, IValue> getModuleOptionsAsMap(){ Map<String,IValue> result = new HashMap<>(); for(Option option : moduleOptions){ result.put(option.name, option.currentValue); } return result; } /** * Convert a textual source locations that is either: * - a slash-separated path (absolute or relative) * - a Rascal source location enclosed between | and |. * * @param loc string representation of a location * @return the loc converted to a source location value */ ISourceLocation convertLoc(String loc){ if(loc.startsWith("|") && loc.endsWith("|")){ TypeStore store = new TypeStore(); Type start = TypeFactory.getInstance().sourceLocationType(); try (StringReader in = new StringReader(loc)) { return (ISourceLocation) new StandardTextReader().read(vf, store, start, in); } catch (FactTypeUseException e) { printUsageAndExit(e.getMessage()); } catch (IOException e) { printUsageAndExit(e.getMessage()); } } else { return URIUtil.correctLocation(loc.startsWith("/") ? "file" : "cwd", "", loc); } return null; } public ISourceLocation getDefaultStdLocation(){ try { return vf.sourceLocation("std", "", ""); } catch (URISyntaxException e) { printUsageAndExit("Cannot create default location: " + e.getMessage()); return null; } } public ISourceLocation getDefaultCourseLocation(){ try { return vf.sourceLocation("courses", "", ""); } catch (URISyntaxException e) { printUsageAndExit("Cannot create default course location: " + e.getMessage()); return null; } } public IList getDefaultStdlocs(){ return vf.list(getDefaultStdLocation()); } public IList getDefaultCourses(){ return vf.list(getDefaultCourseLocation()); } public ISourceLocation getKernelLocation(){ ISourceLocation boot = getCommandLocOption("boot"); return RascalExecutionContext.getKernel(boot); } public ISourceLocation getDefaultBootLocation(){ try { return vf.sourceLocation("boot", "", ""); } catch (URISyntaxException e) { printUsageAndExit("Cannot create default location: " + e.getMessage()); return null; } } public ISourceLocation getDefaultRelocLocation(){ try { return vf.sourceLocation("noreloc", "", ""); } catch (URISyntaxException e) { printUsageAndExit("Cannot create default location: " + e.getMessage()); return null; } } public PathConfig getPathConfig(){ return new PathConfig(getCommandLocsOption("src"), getCommandLocsOption("lib"), getCommandLocOption("bin"), getCommandLocOption("boot")); } public CommandOptions noModuleArgument() { return this; } } class Option { final OptionType optionType; final String name; IValue initialValue; IValue currentValue; final Object defaultValue; final boolean respectsNoDefaults; String helpText; Option(OptionType optionType, String name, IValue initialValue, Object defaultValue, boolean respectsNoDefaults, String helpText){ this.optionType = optionType; this.name = name; this.initialValue = initialValue; this.currentValue = initialValue; this.defaultValue = defaultValue; this.respectsNoDefaults = respectsNoDefaults; this.helpText = helpText; } public boolean set(OptionType optionType, String name, IValue newValue){ if(this.optionType == optionType && this.name.equals(name)){ if(currentValue == initialValue){ currentValue = newValue; return true; } } return false; } public boolean update(OptionType optionType, String name, Function<IValue, IValue> updater) { if(this.optionType == optionType && this.name.equals(name)){ if(currentValue == null){ currentValue = initialValue; } currentValue = updater.apply(currentValue); return true; } return false; } public boolean provides(OptionType optionType, String name){ return this.optionType == optionType && this.name.equals(name); } public IValue get(OptionType optionType, String name) { if(this.optionType.equals(optionType) && this.name.equals(name)){ if(currentValue != null){ return currentValue; } throw new RuntimeException("Option " + name + " has undefined value"); } return null; } @SuppressWarnings("unchecked") public boolean checkDefault(CommandOptions commandOptions){ boolean noDefaults = commandOptions.commandOptions.contains(OptionType.BOOL, "noDefaults") && commandOptions.getCommandBoolOption("noDefaults"); if(currentValue == initialValue){ if(noDefaults && respectsNoDefaults){ throw new RuntimeException("Option " + name + " requires a value"); } if(defaultValue != null){ // type check has been done at creation if(defaultValue instanceof Function<?,?>){ currentValue = ((Function<CommandOptions,IValue>) defaultValue).apply(commandOptions); } else { currentValue = (IValue) defaultValue; } } } if(currentValue == null){ throw new RuntimeException("Option " + name + " requires a value"); } return true; } String help(){ String res = "--" + name; switch(optionType){ case INT: res += " <int>"; break; case STR: res += " <str>"; break; case LOC: res += " <loc>"; break; case LOCS: res += " <locs>"; break; case BOOL: break; } return defaultValue == null ? " " + res : " [" + res + "]"; } } class Options implements Iterable<Option>{ ArrayList<Option> options = new ArrayList<>(); Options add(Option option){ options.add(option); return this; } public IValue get(OptionType optionType, String name) { for(Option option : options){ IValue v = option.get(optionType, name); if(v != null){ return v; } } throw new RuntimeException("Option " + name + " has not been declared"); } public boolean hasNonDefaultValue(OptionType optionType, String name) { for(Option option : options){ if(option.provides(optionType, name)){ return option.currentValue != option.initialValue; } } return false; } public boolean contains(OptionType optionType, String name){ for(Option option : options){ if(option.provides(optionType, name)){ return true; } } return false; } public boolean set(OptionType optionType, String name, IValue newValue){ for(Option option : options){ if(option.set(optionType, name, newValue)){ return true; } } throw new RuntimeException("Option " + name + " could not be set"); } public void update(OptionType optionType, String name, Function<IValue, IValue> updater){ for(Option option : options){ if(option.update(optionType, name, updater)){ return; } } throw new RuntimeException("Option " + name + " could not be updated"); } public List<String> getAllRespectNoDefaults(){ List<String> result = new ArrayList<>(); for(Option option : options){ if(option.respectsNoDefaults){ result.add(option.name); } } return result; } @Override public Iterator<Option> iterator() { return options.iterator(); } }