001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 002// The Molly framework is freely distributable under the terms of an 003// MIT-style license. For details, see the molly pages web site at: 004// http://www.mollypages.org/. Use, modify, have fun ! 005 006package fc.util; 007 008import java.io.*; 009import java.util.*; 010import java.text.*; 011 012import fc.io.*; 013 014/** 015Manages arguments passed to an application. Arguments can be optional or 016required. Arguments received by this class have to be either of the 017following formats: 018<blockquote> 019<tt> 020-flagname1 value 021-flagname2=value2 022</tt>. 023</blockquote> 024Arguments consist of flag name and flag value pairs. All flags must 025begin with a <tt>'-'</tt> character and any new word beginning 026with a <tt>'-'</tt> character is seen as the start of a new flag name 027(the <tt>'-'</tt> character is always permissible <b>inside</b> a 028token, i.e., after the first character). Additionaly, a flag name is 029delimited from it's corresponding value by either a space or the 030<tt>'='</tt> character. 031<p>Note, in the above example, <tt>- flagname1</tt> is separated from 032it's value by a space whereas <tt>- flagname2</tt> is seperated from 033it's value by a <tt>'='</tt> character. Both delimiters are valid and 034can be intermixed on the same line. 035</p><p> 036Note also that <b>if the delimiter is a space, then the value part cannot 037begin with the <tt>-</tt> character</b>, but if the delimiter is <tt>=</tt>, 038then the value part can begin with the <tt>-</tt> character. That is, the 039following input is <b>valid</b>: 040<blockquote><tt>-flagname1=-value-for-flag1</tt></blockquote> 041<p>whereas this is <b>invalid</b>: 042<blockquote><tt>-flagname1 -value-for-flag1</tt></blockquote> 043Here, <tt>-value-for-flag1</tt> is seen as a new flag itself. 044</p><p> 045Following a flag name, all following values are assigned to that flagname, 046up until another flagname is encountered or the end of input is reached. 047Therefore, spaces do not have to be quoted. The following example, 048<blockquote> 049<tt>-path \some file\some where else -port 80</tt> 050</blockquote> 051is parsed as: 052<dl> 053<dt><tt><b>path</b></tt></dt> 054 <dd><tt>\some file\some where else</tt></dd> 055<dt><tt><b>port</b></tt></dt> 056 <dd><tt>80</tt></dd> 057</dl> 058</p><p> 059Different operating systems (and their command interpreters) also 060transform/modify command lines in various ways. This class (and 061Java/JVM) can only parse arguments that it recieves from the operating 062system and those may be different than what you may have specified on 063the command line, due to the aforementioned transformation. The 064following example, 065<blockquote> 066<tt>-path "/foo/bar /baz" -anotherpath \"a/b c/\"</tt> 067</blockquote> 068is parsed as: 069<dl> 070<dt><tt><b>path</b></tt></dt> 071 <dd><tt>/foo/bar /baz</tt></dd> 072<dt><tt><b>anotherpath</b></tt></dt> 073 <dd><tt>"a/b c/"</tt></dd> 074</dl> 075Note, that the <tt>path</tt> value has lost it's double quotes whereas 076the <tt>anotherpath</tt> value has not. This is because the O.S., in this case, 077stripped the quotes for <tt>path</tt> since they were not escaped on the 078command line. Note also that various unix'en convert command line args into 079space delimited tokens. <code>bash</code> for example tokenizes the lines 080using the IFS variable, and then feeds the tokens (separated by space) to the 081invoked program, in this case, the java interpreter. 082</p><p> 083Sometimes flags are specified <b>without</b> any corresponding value, with the 084existence of the flag itself implying some condition. However for clarity, 085it's sometimes better to explicitly set an option to yes or no. Therefore 086instead of saying <tt>-flag</tt>, always say <tt>-flag=[yes|no]</tt>, with 087the value part being "yes" or "no". However, the existence of the flag by 088itself <b>can</b> be simply checked by using the <code>flagExists()</code> method. 089</p><p> 090Typical usage of this class may look like: 091<xmp> 092//inside the main method 093Args myargs = new Args(args); 094args.setUsage("java myApp -port 80 -foo=moo -debugport=5432"); //to inform user 095int port = args.getRequired("port"); //get required "-port" flag 096String abc = args.get("foo"); //get optional "-foo" flag 097String bar = args.get("bar","5000"); //get optional "bar" flag, default to "5000" 098</xmp> 099</p> 100 101Thread Safety: This class <b>is</b> thread safe and can be used by multiple 102threads after it's instantiated. 103 104@author hursh jain 105@version 1.0 7/15/2001 106**/ 107public class Args 108{ 109static boolean _internal_testing = false; 110 111protected String usage; 112protected String[] input; 113protected Map flagMap; 114 115/** 116If a flag name is not well formed, then the corresponding value (if any) 117is stored under this name. For example: <blockquote><tt>- abc</tt></blockquote> 118is missing a flagname (<tt>'-'</tt> followed by whitespace), therefore the 119value <tt>abc</tt> is stored under this default name. 120**/ 121public static String DEFAULT_FLAGNAME = "default"; 122 123/** 124If a flag name is repeated, then all corresponding values for that name are 125concatenated with each other and the concatenated values are <b>delimited</b> 126by this value. It's value in this class is <tt>,</tt>. For example: 127<blockquote><tt>-foo abc -foo xyz</tt></blockquote> has the same flag repeated 128twice (<tt>foo</tt>) and the resulting value for this flag will be <blockquote> 129<tt>abc<b>,</b>xyz</tt></blockquote> 130**/ 131public static String FLAG_REPEAT_VALUE_DELIM = ","; 132 133/** 134Creates a new instance, with the specified String[] to read arguments from. 135Typically, the <tt>static void main(String[] args)</tt> method gets a String[] 136array from the JVM, containing the arguments passed to the application. This 137array will typically be be passed to this constructor. 138**/ 139public Args(String args[]) 140 { 141 input = args; 142 parseFlags(); 143 } 144 145/** 146Creates a new instance, with the specified InputStream to read arguments from. 147Reads as much as possible from the specified stream, so as to not block and 148delay the construction of this object. Any data not available via the specified 149InputStream at the time of construction, is not used later on and is essentially 150lost. The stream encoding is assumed to be ISO-8859-1, i.e., the flags names 151and values read from the stream are treated as normal ascii characters. Be aware 152of differences between parsing command line args and parsing args from a file, 153because typically command line args are modified by the shell before being passed 154to the application. 155**/ 156public Args(InputStream in) 157 { 158 toStringArray(in); 159 parseFlags(); 160 } 161 162 163/** 164Checks if the specified flag exits, regardless of whether a corresponding 165value exist for that flag. For example, suppose the invoked arguments looked 166like: 167<blockquote><tt>-foo -bar=baz</tt></blockquote>. Then the flag <tt>foo</tt> 168would exist but have no corresponding value. 169@param flagname the flagname to check 170@return true if flag exists, false otherwise 171**/ 172public boolean flagExists(String flagname) 173 { 174 boolean exists = false; 175 if (this.flagMap.containsKey(flagname)) 176 { 177 exists = true; 178 } 179 return exists; 180 } 181 182/** Returns the total number of flags available**/ 183public int getFlagCount() { 184 return flagMap.size(); 185 } 186 187/** 188Returns the fully qualified name of the class that contained the <tt>main()</tt> method used to 189invoke the application. <b>Note: </b> this method must be called from the same thread (the "main" 190thread) that started the application. Newly started threads that call this method will instead 191obtain the class name containing that thread's <tt>run()</tt> method. 192**/ 193public String getMainClassName() 194 { 195 return ClassUtil.getMainClassName(); 196 } 197 198 199/** 200Returns an argument by positional value. <tt>get(n)</tt> 201is the same as <tt>static void main(String[] args) --> args[n] 202</tt> and is provided here for convenience. 203 204@param n the 0-based index of the String[] passed to main() 205@throws NullPointerException 206 if the specified index is greater than the number 207 of arguments 208**/ 209public String get(int n) { 210 return input[n]; 211 } 212 213/** 214Returns a required argument by positional value. <tt>get(n)</tt> 215is the same as <tt>static void main(String[] args) --> args[n] 216</tt> and is provided here for convenience. Ff the specified 217index is greater than the number of arguments, then the error 218is handled in the same way as {@link #getRequired(String)} 219 220@param n the 0-based index of the String[] passed to main() 221**/ 222public String getRequired(int n) { 223 if (n < 0 || n > (input.length - 1)) { 224 handleError("Missing required argument at position [" + n + "]"); 225 } 226 return input[n]; 227 } 228 229 230/** 231Returns the value corresponding to the specified flag. 232 233@param flagname the name of the flag <b>without</b> the leading <tt>-</tt> character 234@return <tt>null</tt> either if the flag itself was not found, or if the 235 flag was found but no associated value was found. 236**/ 237public String get(String flagname) 238 { 239 return (String) flagMap.get(flagname); 240 } 241 242/** 243Returns the value corresponding to the specified flag. If 244the flag and/or it's value is not found, this method returns 245the specified backup value instead. 246 247@param flagname the name of the flag <b>without</b> the leading <tt>-</tt> character 248@param backup the value to return if flag not found 249@return value of specified flag or backup string 250**/ 251public String get(String flagname, String backup) 252 { 253 String val = get(flagname); 254 if (val == null) 255 { 256 return backup; 257 } 258 else return val; 259 } 260 261 262/** 263Returns the value corresponding to the specified flag. If 264the flag and/or it's value is not found, this method returns 265the specified backup value instead. 266 267If the property <b>is</b> present, then the corresponding 268value is <tt>true</tt> if the property value is any of the 269following (case-insensitive): 270<blockquote> 271<code> 272yes, 1, true 273</code> 274<blockquote> 275else <tt>false</tt> is returned. Also see {@link #flagExists}. 276 277@param flagname the name of the flag <b>without</b> 278 the leading <tt>-</tt> character 279@param backup value to return if the property for the 280 specified property is <b>not</b> present 281 282@return value of specified key or backup string 283*/ 284public boolean getBoolean(String flagname, boolean backup) 285 { 286 String val = get(flagname); 287 288 if (val == null) 289 return backup; 290 else 291 return toBoolean(val); 292 } 293 294 295/** 296Returns the value corresponding to the specified flag. If the flag and/or it's value is not found 297or is found but cannot be converted into an integer (via a {@link Integer.parseInt(String)} call), 298the backup value will be returned. 299 300@param flagname the name of the flag <b>without</b> the leading <tt>-</tt> character 301@param backup value to return if the property for the specified property is <b>not</b> 302 present 303 304@return value of specified key or backup string 305*/ 306public int getInt(String flagname, int backup) 307 { 308 String val = get(flagname); 309 if (val != null) 310 { 311 try { 312 return Integer.parseInt(val); 313 } 314 catch (NumberFormatException e) { 315 System.err.println("Cannot convert flag '" + flagname + "', into a number, returning backup value " + backup); 316 } 317 } 318 return backup; 319 } 320 321 322/** 323Returns the value corresponding to the specified flag. If the flag and/or it's value is not found 324or is found but cannot be converted into an long (via a {@link Long.parseLong(String)} call), 325the backup value will be returned. 326 327@param flagname the name of the flag <b>without</b> the leading <tt>-</tt> character 328@param backup value to return if the property for the specified property is <b>not</b> 329 present 330 331@return value of specified key or backup string 332*/ 333public long getLong(String flagname, long backup) 334 { 335 String val = get(flagname); 336 if (val != null) 337 { 338 try { 339 return Long.parseLong(val); 340 } 341 catch (NumberFormatException e) { 342 System.err.println("Cannot convert flag '" + flagname + "', into a number, returning backup value " + backup); 343 } 344 } 345 return backup; 346 } 347 348 349/** 350Returns the value corresponding to the specified flag. If the flag's value is not found (even if 351the flag name exists), this method calls the <code>error method</code>, which by default prints a 352stack trace and exits the application. 353 354<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will 355return <tt>null</tt> if the specified flag is not found. 356 357@param flagname the name of the flag <b>without</b> the leading <tt>'-'</tt> character 358@return value of specified flag 359@see #handleError 360**/ 361public String getRequired(String flagname) 362 { 363 if (flagname.equals("")) { 364 handleError("This program expects invocation flag(s)."); 365 return null; 366 } 367 368 String val = get(flagname); 369 if (val == null) { 370 handleError("Missing required flag: " + flagname); 371 } 372 373 return val; 374 } 375 376/** another alias for {@link getRequired(String)} */ 377public String getRequiredString(String flagname) 378 { 379 return getRequired(flagname); 380 } 381 382/* 383Returns the value obtained via {@link getRequired(String)} as a boolean. The boolean value 384returned is <code>true</code> if the property value is any of the following (case-insensitive): 385<blockquote> 386<code> 387yes, 1, true 388</code> 389<blockquote> 390else <tt>false</tt> is returned. 391 392<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will 393return <tt>false</tt> if the specified flag is not found. 394 395@param flagname the name of the flag <b>without</b> the leading <tt>'-'</tt> character 396@return value of specified flag as a boolean 397@see #handleError 398*/ 399public boolean getRequiredBoolean(String flagname) 400 { 401 String val = getRequired(flagname); 402 return toBoolean(val); 403 } 404 405 406/** 407Returns the value obtained via {@link getRequired(String)} as a File. 408 409<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will 410return <tt>false</tt> if the specified file does not exist. 411*/ 412public File getRequiredFile(String flagname) 413 { 414 String val = getRequired(flagname); 415 File f = new File(val); 416 if (! f.exists()) { 417 handleError(flagname + " does not exist/is not a valid file"); 418 } 419 return f; 420 } 421 422/* 423Returns the value obtained via {@link getRequired(String)} as as an integer. If the property cannot 424be converted into an integer (via a {@link Integer.parseInt(String)} call), the resulting action 425would be the same as if the required flag was not present. 426 427<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will 428return <tt>0</tt> if the specified flag is not found. 429 430@param flagname the name of the flag <b>without</b> the leading <tt>'-'</tt> character 431@return value of specified flag as a int 432@see #handleError 433*/ 434public int getRequiredInt(String flagname) 435 { 436 String val = getRequired(flagname); 437 438 int result = 0; 439 440 try { 441 result = Integer.parseInt(val); 442 } 443 catch (NumberFormatException ne) { 444 System.err.println("Cannot convert flag '" + flagname + "', into a number"); 445 handleError(flagname); 446 } 447 448 return result; 449 } 450 451 452/** another alias for {@link getRequiredInt(String)} */ 453public int getRequiredInteger(String flagname) 454 { 455 return getRequiredInt(flagname); 456 } 457 458 459/* 460Returns the value obtained via {@link getRequired(String)} as as an long. If the property cannot 461be converted into an long (via a {@link Long.parseLong(String)} call), the resulting action 462would be the same as if the required flag was not present. 463 464<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will 465return <tt>0</tt> if the specified flag is not found. 466 467@param flagname the name of the flag <b>without</b> the leading <tt>'-'</tt> character 468@return value of specified flag as a long 469@see #handleError 470*/ 471public long getRequiredLong(String flagname) 472 { 473 String val = getRequired(flagname); 474 475 long result = 0; 476 477 try { 478 result = Long.parseLong(val); 479 } 480 catch (NumberFormatException ne) { 481 System.err.println("Cannot convert flag '" + flagname + "', into a number"); 482 handleError(flagname); 483 } 484 485 return result; 486 } 487 488 489/** 490Returns the value as a Date. Understands the following (case-insensitive) values 491<dl> 492<dt><tt>now</tt>, <tt>today</tt></dt> 493 <dd>today's date (this instant)</dd> 494<dt><tt>tomorrow</tt></dt> 495 <dd>tomorrow's date (24 hours from now)</dd> 496<dt><tt>nextweek</tt></dt> 497 <dd>a date representing 1 week from now</dd> 498<dt><tt>atweekbegin</tt>, <tt>atbeginweek</tt>, <tt>atbeginofweek</tt></dt> 499 <dd>first day of the current week</dd> 500</dl> 501<p> 502If none of these special keywords are found, returns the value by trying to parsing 503it as: <tt>yyyy-MM-dd HH:mm:ss</tt> or <tt>yyyy-MM-dd</tt> 504</p><p> 505If the date cannot be parsed using either of the above patterns, an exception is 506thrown. 507*/ 508public Date getRequiredFriendlyDate(String flagname) 509 { 510 Calendar cal = Calendar.getInstance(Locale.US); 511 512 java.util.Date date = null; 513 514 String flagval = getRequired(flagname); 515 flagval = flagval.toLowerCase().intern(); 516 517 if (flagval == "now" || flagval == "today") { 518 date = new java.util.Date(); 519 } 520 else if (flagval == "tomorrow") { 521 date = new java.util.Date(System.currentTimeMillis() + CalendarUtil.TWENTY_FOUR_HOUR); 522 } 523 else if (flagval == "nextweek") { 524 date = new java.util.Date(System.currentTimeMillis() + CalendarUtil.ONE_WEEK); 525 } 526 else if (flagval == "atweekbegin" || flagval == "atbeginweek" 527 || flagval == "atbeginofweek") { 528 date = CalendarUtil.getFirstDayOfWeek(cal, new Date()); 529 } 530 else{ 531 try { 532 date = parseDate(flagval); 533 } 534 catch (ParseException e) { 535 handleError("Cannot parse flag name=["+ flagname + "], value=[" + flagval + "] as a friendly date.."); 536 } 537 } 538 539 return date; 540 } 541 542 543/** 544Returns the value as a Date. Understands the following (case-insensitive) values 545<dl> 546<dt><tt>now</tt>, <tt>today</tt></dt> 547 <dd>today's date (this instant)</dd> 548<dt><tt>tomorrow</tt></dt> 549 <dd>tomorrow's date (24 hours from now)</dd> 550<dt><tt>nextweek</tt></dt> 551 <dd>a date representing 1 week from now</dd> 552<dt><tt>atweekbegin</tt>, <tt>atbeginweek</tt>, <tt>atbeginofweek</tt></dt> 553 <dd>first day of the current week</dd> 554</dl> 555<p> 556If none of these special keywords are found, returns the value by trying to parsing 557it as: <tt>yyyy-MM-dd HH:mm:ss</tt> or <tt>yyyy-MM-dd</tt> 558</p><p> 559The flag is optional but if it is present and cannot be parsed using either of the above patterns, an exception is 560thrown. 561*/ 562public Date getFriendlyDate(String flagname) 563 { 564 Calendar cal = Calendar.getInstance(Locale.US); 565 566 java.util.Date date = null; 567 568 String flagval = get(flagname); 569 if (flagval == null) { 570 return null; 571 } 572 573 return getRequiredFriendlyDate(flagname); 574 } 575 576 577/** 578Checks to see that one and only 1 of the specified flags is present. Useful for mutualy exclusive arguments. 579*/ 580public void onlyOneOf(String... flags) 581 { 582 if (flags == null) { 583 throw new IllegalArgumentException("Specify at least one argument to this method!"); 584 } 585 586 BitSet b = new BitSet(flags.length); 587 588 for (int n = 0; n < flags.length; n++) { 589 if (flagExists(flags[n])) { 590 b.set(n); 591 } 592 } 593 594 if (! (b.cardinality() == 1)) { 595 handleError( 596 b.cardinality() + " mutually exclusive flags specified. Can only specify one of the following: \n " + Arrays.toString(flags) + "\n"); 597 } 598 } 599 600/** 601Checks to see that one and only 1 of the specified flags is present. Useful for mutualy exclusive arguments. 602Alias for the {@link onlyOneOf} method. 603*/ 604public void isMutuallyExclusive(String... flags) 605 { 606 onlyOneOf(flags); 607 } 608 609 610private static java.util.Date parseDate(String s) throws ParseException 611 { 612 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 613 SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd"); 614 615 java.util.Date date = null; 616 try { 617 date = df.parse(s); 618 } 619 catch (ParseException e) { 620 date = df2.parse(s); 621 } 622 623 return date; 624 } 625 626 627/** 628Returns the raw String[] passed to this object during construction. If 629an InputStream was passed in via construction, returns a String[] created 630from that InputStream (tokenized by whitespace). The returned String[] 631is modifiable but it's modification will not affect any other method in 632this class. 633@return String[] of unparsed values 634**/ 635public String[] rawValues() 636 { 637 return this.input; 638 } 639 640 641/** 642Returns a Map containing all flagname/flagvalue pairs. The returned 643map is an <b>unmodifiable</b> view of the map contained in this class. 644@return Map of flagnames/values 645**/ 646public Map values() 647 { 648 return Collections.unmodifiableMap(this.flagMap); 649 } 650 651 652/** 653Specify program usage information to be output when an error occurs. This 654information should contain a short description of expected and/or optional 655flags and information that the application expects. 656@param str Usage information 657**/ 658public void setUsage(String str) 659 { 660 this.usage = str; 661 } 662 663/** 664Convenience method to display the usage information in case 665of program error. Reads and sets the usage file found in the 666same directory/resource location as the calling object's 667package. For example, if clazz <tt>a.b.foo</tt> calls this 668method, then the usage file should exist in the <tt>a/b</tt> 669directory (typically starting from the classpath or a 670jar/zip file). 671<p> 672The name of the usage file is the same as the class name of 673the specified object (without the ending ".java") appended 674with "<tt>_usage.txt</tt>". Therefore the usage file for 675code residing in <tt>a.b.foo.java</tt> should be called 676<tt>foo_usage.txt</tt> and reside in the <tt>a/b</tt> 677directory. 678 679@param caller the calling object <i>or</i> {@link java.lang.Class} Class 680 corresponding to that object 681**/ 682public void setDefaultUsage(Object caller) 683 { 684 try { 685 Class clazz = caller.getClass(); 686 687 String usageFile = StringUtil.fileName(clazz.getName().replace('.', '/')) + "_usage.txt"; 688 689 InputStream in = clazz.getResourceAsStream(usageFile); 690 if (in != null) 691 setUsage( IOUtil.inputStreamToString(in, false) ); 692 else 693 setUsage( "Cannot display usage instructions, " + usageFile + " not found"); 694 } 695 catch (IOException e) { 696 e.printStackTrace(); 697 } 698 } 699 700/** 701Convenience method to display the usage information in case of 702program error. Reads and sets the usage file found specified by 703the <tt>pathToFile</tt> argument of this method. This path should 704be a path to a file reachable from any directory in the classpath 705(i.e., relative to the classpath) and should not start with a "/" 706**/ 707public void setDefaultUsage(String pathToFile) 708 { 709 try { 710 InputStream in = ClassLoader.getSystemResourceAsStream(pathToFile); 711 if (in != null) 712 setUsage( IOUtil.inputStreamToString(in, false) ); 713 else 714 setUsage( "Cannot display usage instructions, " + pathToFile + " not found"); 715 } 716 catch (IOException e) { 717 e.printStackTrace(); 718 } 719 } 720 721/** 722Invoking this method gives the same results as invoking 723{@link #getRequired(String) getRequired} with a non-existent flag name. 724**/ 725public void showError() { 726 getRequired(""); 727 } 728 729/** 730Returns the PrintStream where error messages will be sent. This 731method can be overriden in sublasses to return a different PrintStream. 732**/ 733protected PrintStream getOutputStream() 734 { 735 return System.err; 736 } 737 738final boolean toBoolean(String val) 739 { 740 val = val.trim().toLowerCase().intern(); 741 if ( val == "true" || val == "yes" || val == "1" ) 742 return true; 743 return false; 744 } 745 746/** 747Handles an error situation such as when a required flag is 748not found. By default, calls the <tt>getOutputStream</tt> method, 749prints an error message and exits the JVM by calling <tt>System.exit(1)</tt> 750This method can be overriden in subclasses to handle errors differently. 751@param str the error message 752@see #getOutputStream 753**/ 754protected void handleError(String str) 755 { 756 PrintStream out = getOutputStream(); 757 out.println(str); 758 if (usage != null) { 759 out.println(usage); 760 } 761 out.println(); 762 out.println("-------- Stack Trace --------"); 763 new Exception().printStackTrace(out); 764 if (! _internal_testing) { 765 System.exit(1); 766 } 767 } 768 769public static void main(String[] args) throws Exception 770 { 771 new Test(args); 772 } 773 774static private class Test 775{ 776Test(String[] args) throws Exception 777 { 778 Args myarg = new Args(args); 779 System.out.println("Main Class Name: " + myarg.getMainClassName()); 780 System.out.println("Command line parsed as: "+myarg.values()); 781 782 String testbytes = 783 "-port 80 -port2=5000 -flow c:\\program files -foo " + 784 "-moo \"moo moo\" -x aaa -x bbb - default fasf"; 785 786 Args myarg2 = new Args(new ByteArrayInputStream(testbytes.getBytes("ISO-8859-1"))); 787 Map values = myarg2.values(); 788 System.out.println("testbyte string parsed as: " + values); 789 try { 790 values.put("foo","bar"); 791 } 792 catch (Exception e) { e.printStackTrace(); } 793 String[] rawvals = myarg2.rawValues(); 794 System.out.println("raw values="+Arrays.asList(rawvals)); 795 System.out.println("get(port2)="+myarg2.get("port2")); 796 System.out.println("get(default)="+myarg2.get(DEFAULT_FLAGNAME)); 797 System.out.println("get(x)="+myarg2.get("x")); 798 System.out.println("flagExists(moo)="+myarg2.flagExists("flow")); 799 System.out.println("flagExists(moo)="+myarg2.flagExists("foo")); 800 System.out.println("flagExists(doesnotexits)="+myarg2.flagExists("doesnotexist")); 801 802 testbytes = "-testdate now -testdate2 nextweek -testdate3 atbeginweek -testdate4 2015-03-01"; 803 Args myarg3 = new Args(new ByteArrayInputStream(testbytes.getBytes("ISO-8859-1"))); 804 System.out.println("friendly date test, using as input:\n\t" + testbytes); 805 System.out.println("now="+new Date()); 806 System.out.println("testdate1="+myarg3.getRequiredFriendlyDate("testdate")); 807 System.out.println("testdate2="+myarg3.getRequiredFriendlyDate("testdate2")); 808 System.out.println("testdate3="+myarg3.getRequiredFriendlyDate("testdate3")); 809 System.out.println("testdate4="+myarg3.getRequiredFriendlyDate("testdate4")); 810 811 //prevents system.exit being called. 812 _internal_testing = true; 813 814 try { 815 myarg2.setUsage("Usage: java Args.java your arguments here"); 816 myarg2.getRequired("doesnotexist"); 817 } 818 catch (Exception e) { 819 System.out.println(IOUtil.throwableToString(e)); 820 } 821 822 try { 823 testbytes = "-flag1 -flag2 -flag3"; 824 Args myarg4 = new Args(new ByteArrayInputStream(testbytes.getBytes("ISO-8859-1"))); 825 826 myarg4.setUsage("Testing only 1 mutually flag present"); 827 System.out.print("this should work...myarg4.onlyOneOf(\"flag1\")"); 828 myarg4.onlyOneOf("flag1"); 829 System.out.println("..yes"); 830 831 System.out.println("this should fail.."); 832 myarg4.onlyOneOf("flag1", "flag2"); 833 } 834 catch (Exception e) { 835 System.out.println(IOUtil.throwableToString(e)); 836 } 837 } 838} //~class Test 839 840//-------------------- implementation helper methods ---------------------- 841 842//converts the inputstream into a whitespace delimited array of tokens 843//and then saves those tokens as the input (a String[]) instance variable 844private void toStringArray(InputStream in) 845 { 846 try { 847 int len = in.available(); 848 byte[] arr = new byte[len]; 849 //will read upto len bytes into arr, shouldn't block 850 new BufferedInputStream(in).read(arr, 0, len); 851 String temp = new String(arr, "ISO-8859-1"); 852 //default tokenizer delimits by all whitespace and does 853 //not return delimiters themselves. 854 StringTokenizer st = new StringTokenizer(temp); 855 int toknum = st.countTokens(); 856 this.input = new String[toknum]; 857 int n = 0; 858 while (st.hasMoreTokens()) { 859 this.input[n++] = st.nextToken().trim(); 860 } 861 } 862 catch (IOException e) { 863 this.input = null; 864 } 865 } 866 867//reads this.input(String[]), parses and stores the arguments in this.flagMap (as 868//key=value pairs). A state machine approach to this is *really* painful, surprisingly 869//so. (i tried it and it was 5 times the lines of code over this method and it was still 870//kinda buggy), while this method, although apparently hacky, works like a charm. 871private void parseFlags() 872 { 873 if (input == null) { 874 return; 875 } 876 flagMap = new HashMap(); 877 String flagname = null, flagvalue = null; 878 int fp = findNextFlag(0); 879 int nextfp = 0; 880 int endvalp = 0; //current flagvalue to be read until this position 881 while (fp != -1) 882 { 883 flagname = input[fp]; 884 flagvalue = null; 885 int eqindex = flagname.indexOf('='); 886 if (eqindex != -1) { 887 flagvalue = flagname.substring(eqindex+1, flagname.length()); 888 flagname = flagname.substring(0, eqindex); 889 } 890 nextfp = findNextFlag(fp+1); 891 endvalp = (nextfp == -1) ? input.length : nextfp; 892 for (int n = fp + 1; n < endvalp; n++) { 893 if (flagvalue == null ) { flagvalue = input[n]; } 894 else { flagvalue += " " + input[n]; } 895 } 896 if (flagname.length() == 1) { 897 flagname = DEFAULT_FLAGNAME; 898 } 899 else { 900 flagname = flagname.substring(1,flagname.length()); 901 } 902 if (flagMap.containsKey(flagname)) { //append if flagvalue already exists 903 flagvalue = flagMap.get(flagname) + FLAG_REPEAT_VALUE_DELIM + flagvalue; 904 } 905 flagMap.put(flagname, flagvalue); 906 fp = nextfp; 907// for debugging 908// System.out.println("flagname="+flagname); 909// System.out.println(eqindex); 910// System.out.println("fp, nextfp, endvalp = "+fp+","+nextfp+","+endvalp); 911// System.out.println("flagname, flagvalue="+flagname +"," + flagvalue); 912 } //~while 913 } //~parseFlags 914 915//finds the position of the next -flag starting from pos or -1 if no flag found 916private int findNextFlag(int pos) 917 { 918 for (int n = pos; n < input.length; n++) 919 { 920// System.out.println("n, input[n]="+n+","+input[n]); 921 if (input[n].startsWith("-")) 922 { 923 return n; 924 } 925 } 926 return -1; 927 } 928 929} //~class Args