//----------------------------------------------------------------------------// // // // C L I // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr; import omr.step.Step; import omr.step.Steps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** * Class {@code CLI} parses and holds the parameters of the command * line interface. * * <p> The command line parameters can be (order and case are not relevant): * <dl> * * <dt> <b>-help</b> </dt> <dd> to print a quick usage help and leave the * application. </dd> * * <dt> <b>-batch</b> </dt> <dd> to run in batch mode, with no user * interface. </dd> * * <dt> <b>-step (STEPNAME | @STEPLIST)+</b> </dt> <dd> to run all the * specified steps (including the steps which are mandatory to get to the * specified ones). 'STEPNAME' can be any one of the step names (the case is * irrelevant) as defined in the {@link Steps} class. These steps will * be performed on each sheet referenced from the command line.</dd> * * <dt> <b>-option (KEY=VALUE | @OPTIONLIST)+</b> </dt> <dd> to specify * the value of some application parameters (that can also be set via the * pull-down menu "Tools|Options"), either by stating the key=value pair or by * referencing (flagged by a @ sign) a file that lists key=value pairs (or * even other files list recursively). * A list file is a simple text file, with one key=value pair per line. * <b>Nota</b>: The syntax used is the Properties syntax, so for example * back-slashes must be escaped.</dd> * * <dt> <b>-script (SCRIPTNAME | @SCRIPTLIST)+</b> </dt> <dd> to specify * some scripts to be read, using the same mechanism than input command belows. * These script files contain actions generally recorded during a previous run. * </dd> * * <dt> <b>-input (FILENAME | @FILELIST)+</b> </dt> <dd> to specify some * image files to be read, either by naming the image file or by referencing * (flagged by a @ sign) a file that lists image files (or even other files * list recursively). * A list file is a simple text file, with one image file name per line.</dd> * * <dt> <b>-pages (PAGE | @PAGELIST)+</b> </dt> <dd> to specify some * specific pages, counted from 1, to be loaded out of the input file.</dd> * * <dt> <b>-bench (DIRNAME | FILENAME)</b> </dt> <dd> to define an output * path to bench data file (or directory). * <b>Nota</b>: If the path refers to an existing directory, each processed * score will output its bench data to a score-specific file created in the * provided directory. Otherwise, all bench data, whatever its related score, * will be written to the provided single file.</dd> * * <strike> * <dt> <b>-midi (DIRNAME | FILENAME)</b> </dt> <dd> to define an output * path to MIDI file (or directory). Same note as for -bench.</dd> * </strike> * * <dt> <b>-print (DIRNAME | FILENAME)</b> </dt> <dd> to define an output * path to PDF file (or directory). Same note as for -bench.</dd> * * <dt> <b>-export (DIRNAME | FILENAME)</b> </dt> <dd> to define an output * path to MusicXML file (or directory). Same note as for -bench.</dd> * * </dd> </dl> * * @author Hervé Bitteur */ public class CLI { //~ Static fields/initializers --------------------------------------------- /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(CLI.class); //~ Enumerations ----------------------------------------------------------- /** For handling cardinality of command parameters */ private static enum Card { //~ Enumeration constant initializers ---------------------------------- /** No parameter expected */ NONE, /** Just a single parameter is * expected */ SINGLE, /** One or several * parameters are expected */ MULTIPLE; } /** For command analysis */ private static enum Command { //~ Enumeration constant initializers ---------------------------------- HELP( "Prints help about application arguments and stops", Card.NONE, null), BATCH( "Specifies to run with no graphic user interface", Card.NONE, null), STEP( "Defines a series of target steps", Card.MULTIPLE, "(STEPNAME|@STEPLIST)+"), OPTION( "Defines a series of key=value constant pairs", Card.MULTIPLE, "(KEY=VALUE|@OPTIONLIST)+"), SCRIPT( "Defines a series of script files to run", Card.MULTIPLE, "(SCRIPTNAME|@SCRIPTLIST)+"), INPUT( "Defines a series of input image files to process", Card.MULTIPLE, "(FILENAME|@FILELIST)+"), PAGES( "Defines a set of specific pages to process", Card.MULTIPLE, "(PAGE|@PAGELIST)+"), BENCH( "Defines an output path to bench data file (or directory)", Card.SINGLE, "(DIRNAME|FILENAME)"), // MIDI( // "Defines an output path to MIDI file (or directory)", // Card.SINGLE, // "(DIRNAME|FILENAME)"), PRINT( "Defines an output path to PDF file (or directory)", Card.SINGLE, "(DIRNAME|FILENAME)"), EXPORT( "Defines an output path to MusicXML file (or directory)", Card.SINGLE, "(DIRNAME|FILENAME)"); //~ Instance fields ---------------------------------------------------- /** Info about command itself */ public final String description; /** Cardinality of the expected parameters */ public final Card card; /** Info about expected command parameters */ public final String params; //~ Constructors ------------------------------------------------------- /** * Creates a Command object. * * @param description description of the command * @param params description of command expected parameters, or * null */ Command (String description, Card card, String params) { this.description = description; this.card = card; this.params = params; } } //~ Instance fields -------------------------------------------------------- /** Name of the program */ private final String toolName; /** The CLI arguments */ private final String[] args; /** The parameters to fill */ private final Parameters parameters; //~ Constructors ----------------------------------------------------------- //-----// // CLI // //-----// /** * Creates a new CLI object. * * @param toolName the program name * @param args the CLI arguments */ public CLI (final String toolName, final String... args) { this.toolName = toolName; this.args = Arrays.copyOf(args, args.length); logger.debug("CLI args: {}", Arrays.toString(args)); parameters = parse(); if (parameters != null) { parameters.setImpliedSteps(); } } //~ Methods ---------------------------------------------------------------- //---------------// // getParameters // //---------------// /** * Parse the CLI arguments and return the populated parameters * structure. * * @return the parsed parameters, or null if failed */ public Parameters getParameters () { return parameters; } //----------// // toString // //----------// @Override public String toString () { StringBuilder sb = new StringBuilder(); for (String arg : args) { sb.append(" ").append(arg); } return sb.toString(); } //---------// // addItem // //---------// /** * Add an item to a provided list, while handling indirections if * needed. * * @param item the item to add, which can be a plain string (which is * simply added to the list) or an indirection * (a string starting by the '@' character) * which denotes a file of items to be recursively added * @param list the collection of items to be augmented */ private void addItem (String item, List<String> list) { // The item may be a plain string or the name of a pack that lists // item(s). This is signalled by a starting '@' character in string if (item.startsWith("@")) { // File with other items inside String pack = item.substring(1); BufferedReader br = null; try { br = new BufferedReader(new FileReader(pack)); String newRef; try { while ((newRef = br.readLine()) != null) { addItem(newRef.trim(), list); } br.close(); } catch (IOException ex) { logger.warn("IO error while reading file ''{}''", pack); } } catch (FileNotFoundException ex) { logger.warn("Cannot find file ''{}''", pack); } finally { if (br != null) { try { br.close(); } catch (Exception ignored) { } } } } else if (item.length() > 0) { // Plain item list.add(item); } } //-----------------// // decodeConstants // //-----------------// /** * Retrieve properties out of the flat sequence of "key = value" * pairs. * * @param constantPairs the flat sequence of key = value pairs * @return the resulting constant properties */ private Properties decodeConstants (List<String> constantPairs) throws IOException { Properties props = new Properties(); // Use a simple string buffer in memory StringBuilder sb = new StringBuilder(); for (String pair : constantPairs) { sb.append(pair).append("\n"); } props.load(new StringReader(sb.toString())); return props; } //-------// // parse // //-------// /** * Parse the CLI arguments and populate the parameters structure. * * @return the populated parameters structure, or null if failed */ private Parameters parse () { // Status of the finite state machine boolean paramNeeded = false; // Are we expecting a param? boolean paramForbidden = false; // Are we not expecting a param? Command command = Command.INPUT; // By default Parameters params = new Parameters(); List<String> optionPairs = new ArrayList<>(); List<String> stepStrings = new ArrayList<>(); List<String> pageStrings = new ArrayList<>(); // Parse all arguments from command line for (int i = 0; i < args.length; i++) { String token = args[i]; if (token.startsWith("-")) { // This is a new command // Check that we were not expecting param(s) if (paramNeeded) { printCommandLine(); stopUsage( "Found no parameter after command '" + command + "'"); return null; } // Remove leading minus sign and switch to uppercase // To recognize command token = token.substring(1).toUpperCase(Locale.ENGLISH); boolean found = false; for (Command cmd : Command.values()) { if (token.equals(cmd.name())) { command = cmd; paramNeeded = command.card != Card.NONE; paramForbidden = !paramNeeded; found = true; break; } } // No command recognized if (!found) { printCommandLine(); stopUsage("Unknown command '-" + token + "'"); return null; } // Commands with no parameters switch (command) { case HELP: { stopUsage(null); return null; } case BATCH: params.batchMode = true; break; } } else { // This is a parameter for the current command // Check we can accept a parameter if (paramForbidden) { printCommandLine(); stopUsage( "Extra parameter '" + token + "' found after command '" + command + "'"); return null; } switch (command) { case STEP: addItem(token, stepStrings); break; case OPTION: addItem(token, optionPairs); break; case SCRIPT: addItem(token, params.scriptNames); break; case INPUT: addItem(token, params.inputNames); break; case PAGES: addItem(token, pageStrings); break; case BENCH: params.benchPath = token; break; case EXPORT: params.exportPath = token; break; // case MIDI : // params.midiPath = token; // // break; case PRINT: params.printPath = token; break; default: } paramNeeded = false; paramForbidden = command.card == Card.SINGLE; } } // Additional error checking if (paramNeeded) { printCommandLine(); stopUsage("Expecting a token after command '" + command + "'"); return null; } // Decode option pairs try { params.options = decodeConstants(optionPairs); } catch (Exception ex) { logger.warn("Error decoding -option ", ex); } // Check step names for (String stepString : stepStrings) { try { // Read a step name params.desiredSteps.add( Steps.valueOf(stepString.toUpperCase())); } catch (Exception ex) { printCommandLine(); stopUsage( "Step name expected, found '" + stepString + "' instead"); return null; } } // Check page ids for (String pageString : pageStrings) { try { // Read a page id (counted from 1) int id = Integer.parseInt(pageString); if (params.pages == null) { params.pages = new TreeSet<>(); } params.pages.add(id); } catch (Exception ex) { printCommandLine(); stopUsage( "Page id expected, found '" + pageString + "' instead"); return null; } } // Results logger.debug(Main.dumping.dumpOf(params)); return params; } //------------------// // printCommandLine // //------------------// /** * Printout the command line with its actual parameters. */ private void printCommandLine () { StringBuilder sb = new StringBuilder("Command line parameters: "); if (toolName != null) { sb.append(toolName).append(" "); } sb.append(this); logger.info(sb.toString()); } //-----------// // stopUsage // //-----------// /** * Printout a message if any, followed by the general syntax for * the command line. * * @param msg the message to print if non null */ private void stopUsage (String msg) { // Print message if any if (msg != null) { logger.warn(msg); } StringBuilder buf = new StringBuilder(); // Print version buf.append("\nVersion:"); buf.append("\n ").append(WellKnowns.TOOL_REF); // Print arguments syntax buf.append("\nArguments syntax:"); for (Command command : Command.values()) { buf.append( String.format( "%n %-36s %s", String.format( " [-%s%s]", command.toString().toLowerCase(Locale.ENGLISH), ((command.params != null) ? (" " + command.params) : "")), command.description)); } // Print all allowed step names buf.append("\n\nKnown step names are in order").append( " (non case-sensitive):"); for (Step step : Steps.values()) { buf.append( String.format( "%n %-11s : %s", step.toString().toUpperCase(), step.getDescription())); } logger.info(buf.toString()); } //~ Inner Classes ---------------------------------------------------------- //------------// // Parameters // //------------// /** * A structure that collects the various parameters parsed out of * the command line. */ public static class Parameters { //~ Instance fields ---------------------------------------------------- /** Flag that indicates a batch mode */ boolean batchMode = false; /** The set of desired steps */ final Set<Step> desiredSteps = new LinkedHashSet<>(); /** The map of options */ Properties options = null; /** The list of script file names to execute */ final List<String> scriptNames = new ArrayList<>(); /** The list of input image file names to load */ final List<String> inputNames = new ArrayList<>(); /** The set of page ids to load */ SortedSet<Integer> pages = null; /** Where log data is to be saved */ ///String logPath = null; /** Where bench data is to be saved */ String benchPath = null; /** Where exported score data (MusicXML) is to be saved */ String exportPath = null; /** Where MIDI data is to be saved */ String midiPath = null; /** Where printed score (PDF) is to be saved */ String printPath = null; //~ Constructors ------------------------------------------------------- private Parameters () { } //~ Methods ------------------------------------------------------------ //-----------------// // setImpliedSteps // //-----------------// /** * Some output parameters require their related step to be set. */ private void setImpliedSteps () { if (exportPath != null) { desiredSteps.add(Steps.valueOf(Steps.EXPORT)); } if (printPath != null) { desiredSteps.add(Steps.valueOf(Steps.PRINT)); } // if (midiPath != null) { // desiredSteps.add(Steps.valueOf(Steps.MIDI)); // } } } }