package org.das2.util; import java.io.File; import java.util.*; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.AbstractPreferences; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; /** * Utility class for processing the String[] arguments passed into the main routine, * handing positional and switch parameters. Also automatically generates the * usage documentation. * * Note in Autoplot's pngwalk, we add a parameter and positional argument with the * same name. This should continue to be supported. */ public class ArgumentList { private int nposition; private final String programName; private final String[] positionKeys; private final Map values; private final Map descriptions; private final Map names; private final Map reverseNames; private final Map formUsed; private final Map abbrevs; private final ArrayList requireOneOfList; private int exitCode= -999; /** * if false, then any unrecognized switch is an error. */ boolean allowUndefinedSwitch= false; private static final String UNSPECIFIED = "__unspecified__"; private static final String REFERENCEWITHOUTVALUE = "__referencewithoutvalue__"; private static final String UNDEFINED_SWITCH = "__undefinedSwitch__"; public static final String FALSE = "__false__"; public static final String TRUE = "__true__"; private static final Logger logger= Logger.getLogger( "das2.util" ); /** * creates the processor for the program. programName is provided * for the usage statement. After creating the object, arguments are * specified one by one, and then the process method is called. * @param programName the program name */ public ArgumentList(String programName) { this.programName= programName; positionKeys= new String[10]; values= new HashMap<>(); descriptions= new HashMap<>(); names= new LinkedHashMap<>(); reverseNames= new HashMap<>(); abbrevs= new HashMap<>(); formUsed= new HashMap<>(); requireOneOfList= new ArrayList<>(); } /** * get the value for this parameter * @param key the argument identifier. * @return the parameter's value. * @throws IllegalArgumentException if the parameter name was never described. */ public String getValue(String key) { if ( values.containsKey(key) ) { return (String)values.get( key ); } else { throw new IllegalArgumentException( "No such key: "+key ); } } /** * make a standard way to make file names absolute * @param ref e.g. files/afile/foo.txt * @return /home/jbf/data/files/afile/foo.txt because PWD is /home/jbf/data */ public static String makeFileReferenceAbsolute( String ref ) { if ( ref.startsWith("file://") ) ref= ref.substring(7); if ( ref.startsWith("file:" ) ) ref= ref.substring(5); if ( ref.startsWith("http://") || ref.startsWith("https://") || ref.startsWith("sftp://") || ref.startsWith("ftp://") ) { throw new IllegalArgumentException("local file reference expected"); } else { File ff= new File(ref); if ( !ff.isAbsolute() ) { return ff.getAbsolutePath(); } else { return ref; } } } /** * returns the options as a java.util.prefs.Preferences object, for * batch processes. The idea is that a process which grabs default * settings from the user Preferences can instead get them from the command * line, to support batch processes. See the Vg1pws app for an example of * how this is used. * * @return a Preferences object, loaded with the command line values. */ public Preferences getPreferences() { return new AbstractPreferences(null,"") { @Override protected void putSpi(String key, String value) { formUsed.put(key,value); values.put(key,value); } @Override protected String getSpi(String key) { if ( formUsed.containsKey(key) ) { return (String) values.get(key); } else { return null; } } @Override protected void removeSpi(String key) { // do nothing } @Override protected void removeNodeSpi() throws BackingStoreException { // do nothing } @Override protected String[] keysSpi() throws BackingStoreException { return (String[])values.keySet().toArray(new String[values.size()]); } @Override protected String[] childrenNamesSpi() throws BackingStoreException { return new String[0]; } @Override protected AbstractPreferences childSpi(String name) { return null; } @Override protected void syncSpi() throws BackingStoreException { // do nothing } protected void flushSpi() throws BackingStoreException { // do nothing } }; } public boolean getBooleanValue(String key) { if ( values.containsKey(key) ) { return values.get( key ) .equals( this.TRUE ); } else { throw new IllegalArgumentException( "No such key: "+key ); } } /** * Specify the ith positional argument. * * @param position the position number, 0 is the first argument position after the class name. * @param key the internal reference name to get the value specified. * @param description a short (40 character) description of the argument. */ public void addPositionArgument(int position, String key, String description) { if ( position>nposition ) { throw new IllegalArgumentException( "Position arguments must be specified 0,1,2,3: position="+position ); } if ( position>positionKeys.length ) { throw new IllegalArgumentException( "Position too big: position="+position ); } nposition= position+1; positionKeys[position]= key; descriptions.put(key, description); values.put( key, UNSPECIFIED ); } /** * requires the user specify one of these values, otherwise the usage * statement is printed. * @param keyNames an array of internal key names that identify parameters. */ public void requireOneOf( String[] keyNames ) { requireOneOfList.add(keyNames); } /** * Specify the ith positional argument, which may be left unspecified by * the user. Note that all positional arguments after this one must also be * optional. * * @param position the position number, 0 is the first argument position after the class name. * @param key the internal reference name to get the value specified. * @param defaultValue the value that is returned if a value is not provided by the user. * @param description a short (40 character) description of the argument. */ public void addOptionalPositionArgument(int position, String key, String defaultValue, String description) { if ( key==null ) throw new IllegalArgumentException("null key"); addPositionArgument( position, key, description ); values.put(key,defaultValue); if ( defaultValue==null ) { if ( false ) System.err.println("breakpoint"); } } /** * specify a named switch argument that must be specified by the user. For example, --level=3 or -l=3 * @param name the long parameter name, which the user may enter. e.g. "level" * @param abbrev short (one letter) parameter version. e.g. "l" for -l=3 * @param key the internal reference name to get the value specified, not necessarily but often the same as name. * @param description a short (40 character) description of the argument. */ public void addSwitchArgument(String name, String abbrev, String key, String description) { if ( abbrev==null && name==null ) { throw new IllegalArgumentException( "both abbrev and name are null, one must be specified" ); } if ( key==null ) throw new IllegalArgumentException("null key"); descriptions.put( key, description ); if ( abbrev!=null ) { if ( abbrevs.containsKey(abbrev) ) { throw new IllegalArgumentException( "abbrev already used: "+abbrev ); } abbrevs.put( abbrev, key ); } if ( name!=null ) { names.put( name, key ); reverseNames.put( key, name ); } values.put( key, UNSPECIFIED ); } /** * specify a named switch argument that may be specified by the user. For example, --level=3 or -l=3 * @param name the long parameter name, which the user may enter. e.g. "level" * @param abbrev short (one letter) parameter version. e.g. "l" for -l=3 * @param defaultValue value to return if the user doesn't specify. If TRUE or FALSE is used, then the * user may use a number of different inputs such as "T" or "true", and getBooleanValue can be used to read the value * @param key the internal reference name to get the value specified, not necessarily but often the same as name. * @param description a short (40 character) description of the argument. */ public void addOptionalSwitchArgument(String name, String abbrev, String key, String defaultValue, String description) { addSwitchArgument( name, abbrev, key, description ); if ( key==null ) throw new IllegalArgumentException("null key"); values.put( key, defaultValue ); if ( defaultValue==null ) { if ( false ) System.err.println("breakpoint"); } } /** * specify a named switch argument that is named, and we only care whether it was used or not. e.g. --debug * @param name the long parameter name, which the user may enter. e.g. "level" * @param abbrev short (one letter) parameter version. e.g. "l" for -l=3 * @param key the internal reference name to get the value specified, not necessarily but often the same as name. * @param description a short (40 character) description of the argument. */ public void addBooleanSwitchArgument(String name, String abbrev, String key, String description) { if ( key.equals("commandLinePrefs") ) allowUndefinedSwitch=true; addOptionalSwitchArgument( name, abbrev, key, FALSE, description ); } /** * print the usage statement out to stderr. */ public void printUsage() { StringBuilder s; s= new StringBuilder( "Usage: " ); s.append(this.programName).append(" "); for ( int i=0; i "); } } System.err.println(s.toString()); Set> sset= names.entrySet(); Iterator> i= sset.iterator(); Map abbrevsCopy= new HashMap<>(abbrevs); while ( i.hasNext() ) { Entry e= i.next(); String name= e.getKey(); String key= e.getValue(); String abbrev=null; for ( Entry se: abbrevsCopy.entrySet() ) { if ( se.getValue().equals(name) ) { abbrev= se.getKey(); break; } } if ( abbrev!=null ) { abbrevsCopy.remove(abbrev); } s= new StringBuilder(" "); String description= descriptions.get(key); String value= values.get(key); if ( abbrev==null ) { if ( !this.UNSPECIFIED.equals(value) ) { if ( this.FALSE.equals(value) || this.TRUE.equals(value) ) { s.append( String.format( "--%s \t%s", name, description ) ); } else { s.append( String.format( "--%s= \t%s ", name, description ) ); } } else { s.append( String.format( "--%s= \t%s (required)", name, description ) ); } } else { if ( !this.UNSPECIFIED.equals(value) ) { if ( this.FALSE.equals(value) || this.TRUE.equals(value) ) { s.append( String.format( "--%s, -%s \t%s", name, abbrev, description ) ); } else { s.append( String.format( "--%s=, -%s= \t%s ", name, abbrev, description ) ); } } else { s.append( String.format( "--%s=, -%s= \t%s (required)", name, abbrev, description ) ); } } System.err.println(s.toString()); } sset= abbrevsCopy.entrySet(); i= sset.iterator(); while ( i.hasNext() ) { Entry e= i.next(); String abbrev= e.getKey(); String key= e.getValue(); s= new StringBuilder(" "); String description= descriptions.get(key); if ( !this.UNSPECIFIED.equals(values.get(key) ) ) { if ( this.FALSE.equals(values.get(key)) || this.TRUE.equals(values.get(key)) ) { s.append( String.format( "-%s \t%s", abbrev, description ) ); } else { s.append( String.format( "-%s=%s ", abbrev, description ) ); } } else { s.append( String.format( "-%s=%s (required)", abbrev, description ) ); } System.err.println(s); } } /** * return 0 if the exit code for a checkArgs()==false is 0 or non-zero. * It will be 0 if --help was used. * @return the exit code. */ public int getExitCode() { return exitCode; } /** * check that the user's specified arguments are valid. * @return false if usage has been printed and the caller should exit. */ private boolean checkArgs() { boolean error= false; java.util.List errorList= new java.util.ArrayList<>(); // add strings to here for ( int i=0; !error & i> ientries= values.entrySet().iterator(); while ( ientries.hasNext() ) { Entry e= ientries.next(); String key= e.getKey(); if ( key==null ) { System.err.println("TODO: handle this case where ever it's coming from: key==null"); continue; } if ( key.equals("help") || key.equals("--help" ) ) { // kludge printUsage(); exitCode= 0; return false; } if ( e.getValue() == this.UNSPECIFIED ) { errorList.add( "Argument needed: --" + reverseNames.get( key ) ); } if ( e.getValue() == this.REFERENCEWITHOUTVALUE ) { errorList.add( "Switch requires argument: "+formUsed.get(key)); } if ( e.getValue() == this.UNDEFINED_SWITCH && !allowUndefinedSwitch ) { errorList.add( "Not a valid switch: "+formUsed.get(key) ); } } } if ( !error ) { for ( int i=0; i0 ) { printUsage(); System.err.println( "" ); for ( int ii=0; ii getMap() { return new HashMap<>( values ); } /** * returns a Map of optional arguments that were specified, so you can see * exactly what was specified. * @return a Map of the specified values, without defaults. */ public Map getOptions() { HashMap result= new HashMap<>(); List exclude= Arrays.asList( positionKeys ); for (Entry e : values.entrySet()) { String key= e.getKey(); if( !exclude.contains(key) && formUsed.containsKey(key) ) { result.put( key, e.getValue() ); } } return result; } private int processSwitch( String[] args, int i ) { String key; if ( args[i].startsWith("--") ) { String name= args[i].substring(2); if ( name.indexOf('=') != -1 ) { name= name.substring(0,name.indexOf('=')); } key= (String)names.get(name); } else { // TODO: should support several abbrevs: e.g., -xvf (Throw exception if ambiguous) String abbrev= args[i].substring(1); if ( abbrev.indexOf('=') != -1 ) { abbrev= abbrev.substring(0,abbrev.indexOf('=')); } key= (String)abbrevs.get(abbrev); } if ( key==null ) { key= args[i]; values.put( key, this.UNDEFINED_SWITCH ); formUsed.put( key, args[i] ); logger.log(Level.FINER, "undefined switch: {0}", key); } else { String value; formUsed.put( key,args[i] ); if ( values.get(key)==this.FALSE || values.get(key)==this.TRUE ) { // is boolean value= TRUE; if ( args[i].indexOf('=')!= -1 ) { value= args[i].substring( args[i].indexOf('=')+1 ); if ( value.equals("t") || value.equals("true") || value.equals("y") || value.equals("yes") ) { value= TRUE; } else if ( value.equals("f") || value.equals("false") || value.equals("n") || value.equals("no") ) { value= FALSE; } } values.put( key, value ); } else { if ( args[i].indexOf('=') != -1 ) { value= args[i].substring( args[i].indexOf('=')+1 ); } else { if ( i+1 < args.length && ( args[i+1].equals("-") || ! args[i+1].startsWith("-") ) ) { value= args[i+1]; i= i+1; } else { value= this.REFERENCEWITHOUTVALUE; } } if ( value.startsWith("\"") ) { value= value.substring(1,value.length()-2); } logger.log(Level.FINER, "switch key: {0}={1}", new Object[]{key, value}); values.put( key, value ); } } return i; } /** * given the specification, process the argument list. If the list is in error, the * usage statement is generated, and the System.exit is called (sorry!). Otherwise * the method returns and getValue() may be used to retrieve the values. * * Again, note that System.exit may be called. This is probably a bad idea and another * method will probably be added that would return true if processing was successful. * * @param args as in public static void main( String[] args ). * @return false if System.exit should be called. */ public boolean process(String[] args) { StringBuilder sb= new StringBuilder(); for (String arg : args) { sb.append(arg); sb.append(" "); } logger.log(Level.FINER, "args: {0}", sb.toString()); int iposition=0; for ( int i=0; i> entries= names.entrySet(); Iterator> i= entries.iterator(); while ( i.hasNext() ) { Entry entry= i.next(); String name= entry.getKey(); String key= entry.getValue(); String value= (String)formUsed.get(key); if ( value !=null ) { if ( value.equals(this.TRUE) ) { s.append( "--" ).append( name ); } if ( value.equals(this.FALSE ) ) { // do nothing } else { s.append( value ).append(" "); } } } return s.toString(); } /** * dump the configuration to the given logger at Level.CONFIG. * @param logger */ public void logPrefsSettings( Logger logger ) { String s= getPrefsSettings(); logger.log(Level.CONFIG,s); } }