Object-Oriented Command Line InterfacesAndrew S. Townley 21-Nov-2004
In this article, I attempt to cover the changes in the
Since the first release of the library, the following features have been added:
The result of these changes is that the library is now easier to use and goes further to encapsulate the operations of individual options than before, making the code easier to write, debug and maintain.
With the 3.0.0 release, using the "classic" command-line
argument handing will be deprecated as an idiom, but I am not
going to actually deprecate the methods. It's still a valid
way to approach the problem, but you don't get any of the
benefits of encapsulation and you still have some really long
and complicated Using the DefaultCommandListener Class
After using the original library for several projects, it
became clear that one of the things I was trying to solve, to
limit the amount of boilerplate code, really was not as good
as it could have been. In practice, I ended up with a lot of
nested inner classes that existed just to provide the array of
Historically, the
Using the 139 private CommandOption[] _mainopts = { 140 _create, _extract, _file, _verbose, _xclude }; 141 142 private CommandOption[] _examples = { _display, _options };
These groups can now be associated with different headings for
the autohelp during the initialization of the
158 _parser.addCommandListener( 159 new DefaultCommandListener("feather options", _mainopts)); 160 _parser.addCommandListener( 161 new DefaultCommandListener("Example options", _examples)); When the program is executed and asked to show the usage summary, the following output is now created: feather options: -c, --create create a new archive -x, --extract extract files from the named archive -f, --file=ARCHIVE specify the name of the archive (default is stdout) -v, --verbose print status information during execution -X, --exclude=[ FILE | DIRECTORY ] exclude the named file or directory from the archive Example options: -display DISPLAY specify the display on which the output should be written -DPROPERTY=VALUE[,PROPERTY=VALUE...] set specific run-time properties Help options: -?, --help show this help message --usage show brief usage message New Internationalization Support
Previous versions of the library used hard-coded English
messages within the Supporting POSIX-style OptionsOne of the features I knew needed to be part of the library to gain wide acceptance was support for POSIX-style options. At preset, these are mostly supported, however the abbreviation of the option name supported by some libraries is not present.
POSIX-style options are options similar to those found in the
X window system, the
Another related feature of POSIX-style options is that you can
combine options which do not require an argument together into
a single "word". The program then must break apart the
options to deal with the separately. Previously, if you
wanted to verbosely create an archive using
Joined, Delimited and Repeatable Options
A command option such as
Often, joined options may have multiple values. Two different
examples of this are supported by the
The second type of example is the various
Both of these usage scenarios are now fully supported by the
classes in the library. Originally, the
More Help from AutoHelp
Another feature present in programs using the GNU
RedHat's
This additional information is provided through the use of a
new attribute of the
Once the preamble/postamble has been set, it will be displayed
as in Figure 2, which was generated from running $ java feather3 --help Usage: feather [OPTION...] FILE... This is the TE-Code feather program. It is used to illustrate the features of the com.townleyenterprises.command package. Examples: # create archive.feather from files one, two, three and four feather -cvf archive.feather one two three four # exclude files five and six from an archive feather -cvf archive.feather -X five -X six one two three All options are not required unless otherwise stated in the description. feather options: -c, --create create a new archive -x, --extract extract files from the named archive -f, --file=ARCHIVE specify the name of the archive (default is stdout) -v, --verbose print status information during execution -X, --exclude=[ FILE | DIRECTORY ] exclude the named file or directory from the archive Example options: -display DISPLAY specify the display on which the output should be written -DPROPERTY=VALUE[,PROPERTY=VALUE...] set specific run-time properties Help options: -?, --help show this help message --usage show brief usage message This utility does not actually create an archive. Any bugs in the software should be reported to the te-code mailing lists. http://te-code.sourceforge.net Using Option ConstraintsHistorically, in command-line applications, a large body of code is devoted to determining if conflicting options have been specified, or if an option had a dependency on another one, that both were specified by the user. In the event that any of these were not true, an error message was given to the user so that they could try again.
With the 3.0.0-pre1 release of the library, this code is
normally not necessary in the majority of instances. Instead
of writing a lot of By default, the library provides the following constraint types (see the API documentation for more information):
Using the capabilities of if(_create.getMatched() && _extract.getMatched()) { System.err.println("error: cannot specify both -x and -c."); System.exit(-500); }
However, the same test can be accomplished by using a
165 _parser.addConstraint( 166 new MutexOptionConstraint(-500, _create, _extract));
The
Probably the other most commonly used constraint is the
64 if((file.getMatched() || xclude.getMatched()) && 65 !create.getMatched()) 66 { 67 System.err.println("error: nothing to do"); 68 clp.usage(); 69 System.exit(-1); 70 }
The code in Listing 5 can be replaced with the code in Listing
6 (from 167 _parser.addConstraint( 168 new RequiresAnyOptionConstraint(-501, _file, 169 new CommandOption[] { _create, _extract }));
In Listing 6, the option constraint indicates that the file
option requires any of the create and extract options, thus
automatically performing the check previously coded in Listing
5. Any number of these constraints can be added and will be
evaluated during the Some examples of the output generated when the constraints are violated is provided in Figure 3. $ java feather3 -cxf archive.tar error: cannot specify both 'create' and 'extract'. Exiting. Usage: feather [-c|--create] [-x|--extract] [-f|--file ARCHIVE] [-v|--verbose] [-X|--exclude [ FILE | DIRECTORY ]] [-display DISPLAY] [-DPROPERTY=VALUE[,PROPERTY=VALUE...]] [-?|--help] [--usage] FILE... $ java feather3 -f archive.tar error: option 'file' requires one of: 'create', 'extract'. Exiting. Usage: feather [-c|--create] [-x|--extract] [-f|--file ARCHIVE] [-v|--verbose] [-X|--exclude [ FILE | DIRECTORY ]] [-display DISPLAY] [-DPROPERTY=VALUE[,PROPERTY=VALUE...]] [-?|--help] [--usage] FILE... $ java feather3 -X some/path error: option 'exclude' requires one of: 'create', 'extract'. Exiting. Usage: feather [-c|--create] [-x|--extract] [-f|--file ARCHIVE] [-v|--verbose] [-X|--exclude [ FILE | DIRECTORY ]] [-display DISPLAY] [-DPROPERTY=VALUE[,PROPERTY=VALUE...]] [-?|--help] [--usage] FILE...
The final option constraint is the
Implementing the Command Pattern
The final step in fully using an OO approach to handling
command-line arguments is to make the
Listing 7 shows the implementation of the create option
instance. Like the other options in the program, the actual
instance is an anonymous inner class rather than a specific
derived class, but, like implementing Swing
61 private CommandOption _create = new CommandOption("create", 62 'c', false, null, 63 "create a new archive") { 64 public void execute() throws Exception 65 { 66 String[] largs = _parser.getUnhandledArguments(); 67 68 if(largs.length == 0) 69 { 70 System.err.println("error: refusing to create empty archive."); 71 _parser.usage(); 72 System.exit(-2); 73 } 74 75 if(_verbose.getMatched() && _file.getMatched()) 76 { 77 System.out.println("creating archive " + _file.getArg()); 78 } 79 80 for(int i = 0; i < largs.length; ++i) 81 { 82 if(_verbose.getMatched()) 83 System.out.println("adding " + largs[i]); 84 } 85 86 if(_xclude.getMatched()) 87 { 88 if(_verbose.getMatched()) 89 { 90 for(Iterator i = _xclude.getArgs().iterator(); i.hasNext();) 91 { 92 System.out.println("excluded " + i.next()); 93 } 94 } 95 } 96 } 97 };
Lines 61-63 create the instance the same as in the previous
version of the example. However, lines 64-96 are the
implementation of the create action. The interactions between
the active
The above sequence does not have any negative effects if some
options do not define an Putting It All Together
Listing 8 shows the entire source for the
1 ////////////////////////////////////////////////////////////////////// 2 // 3 // Copyright (c) 2004, Andrew S. Townley 4 // All rights reserved. 5 // 6 // Redistribution and use in source and binary forms, with or without 7 // modification, are permitted provided that the following conditions 8 // are met: 9 // 10 // * Redistributions of source code must retain the above 11 // copyright notice, this list of conditions and the following 12 // disclaimer. 13 // 14 // * Redistributions in binary form must reproduce the above 15 // copyright notice, this list of conditions and the following 16 // disclaimer in the documentation and/or other materials provided 17 // with the distribution. 18 // 19 // * Neither the names Andrew Townley and Townley Enterprises, 20 // Inc. nor the names of its contributors may be used to endorse 21 // or promote products derived from this software without specific 22 // prior written permission. 23 // 24 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 27 // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 28 // COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 29 // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 32 // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 33 // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 35 // OF THE POSSIBILITY OF SUCH DAMAGE. 36 // 37 // File: feather3.java 38 // Created: Fri Jul 30 16:26:31 IST 2004 39 // 40 ////////////////////////////////////////////////////////////////////// 41 42 // now, you wouldn't do this in actual code, would you??? 43 import com.townleyenterprises.command.*; 44 45 import java.util.Iterator; 46 47 /** 48 * This is an example of a hypothetical file archive program roughly 49 * based on the UNIX tar command. It is intended to illustrate the 50 * proper use of the CommandParser and the command package. 51 * 52 * @version $Id: article-20041121-cli.xml,v 1.1.1.1 2004/11/21 20:37:29 atownley Exp $ 53 * @author Andrew S. Townley 54 * @since 3.0 55 */ 56 57 public class feather3 58 { 59 private CommandParser _parser = null; 60 61 private CommandOption _create = new CommandOption("create", 62 'c', false, null, 63 "create a new archive") { 64 public void execute() throws Exception 65 { 66 String[] largs = _parser.getUnhandledArguments(); 67 68 if(largs.length == 0) 69 { 70 System.err.println("error: refusing to create empty archive."); 71 _parser.usage(); 72 System.exit(-2); 73 } 74 75 if(_verbose.getMatched() && _file.getMatched()) 76 { 77 System.out.println("creating archive " + _file.getArg()); 78 } 79 80 for(int i = 0; i < largs.length; ++i) 81 { 82 if(_verbose.getMatched()) 83 System.out.println("adding " + largs[i]); 84 } 85 86 if(_xclude.getMatched()) 87 { 88 if(_verbose.getMatched()) 89 { 90 for(Iterator i = _xclude.getArgs().iterator(); i.hasNext();) 91 { 92 System.out.println("excluded " + i.next()); 93 } 94 } 95 } 96 } 97 }; 98 99 private CommandOption _extract = new CommandOption( 100 "extract", 101 'x', 102 false, 103 null, 104 "extract files from the named archive"); 105 106 private CommandOption _file = new CommandOption( 107 "file", 108 'f', 109 true, 110 "ARCHIVE", 111 "specify the name of the archive (default is stdout)"); 112 113 private CommandOption _verbose = new CommandOption( 114 "verbose", 115 'v', 116 false, 117 null, 118 "print status information during execution"); 119 120 private RepeatableCommandOption _xclude = new RepeatableCommandOption( 121 "exclude", 122 'X', 123 "[ FILE | DIRECTORY ]", 124 "exclude the named file or directory from the archive"); 125 126 private PosixCommandOption _display = new PosixCommandOption( 127 "display", 128 true, 129 "DISPLAY", 130 "specify the display on which the output should be written"); 131 132 private JoinedCommandOption _options = new JoinedCommandOption( 133 'D', 134 false, 135 "PROPERTY=VALUE[,PROPERTY=VALUE...]", 136 "set specific run-time properties", 137 true); 138 139 private CommandOption[] _mainopts = { 140 _create, _extract, _file, _verbose, _xclude }; 141 142 private CommandOption[] _examples = { _display, _options }; 143 144 public static void main(String[] args) 145 { 146 new feather3(args); 147 } 148 149 private feather3(String[] args) 150 { 151 _parser = new CommandParser("feather", "FILE..."); 152 _parser.setExitOnMissingArg(true, -10); 153 154 // this is ugly and you wouldn't do this in real code, 155 // but it serves to illustrate the method call 156 _parser.setExtraHelpText("SEE ACTUAL SOURCE", "SEE ACTUAL SOURCE"); 157 158 _parser.addCommandListener( 159 new DefaultCommandListener("feather options", _mainopts)); 160 _parser.addCommandListener( 161 new DefaultCommandListener("Example options", _examples)); 162 163 _parser.parse(args); 164 165 _parser.addConstraint( 166 new MutexOptionConstraint(-500, _create, _extract)); 167 _parser.addConstraint( 168 new RequiresAnyOptionConstraint(-501, _file, 169 new CommandOption[] { _create, _extract })); 170 _parser.addConstraint(new RequiresAnyOptionConstraint(-502, 171 _xclude, new CommandOption[] { _create, _extract })); 172 173 try 174 { 175 _parser.executeCommands(); 176 } 177 catch(Exception e) 178 { 179 e.printStackTrace(); 180 System.exit(-111); 181 } 182 } 183 } As previously mentioned, the create command option relies on access to both the parser (to retrieve the arguments to archive in line 66) and to the other arguments (lines 75-95) so that it can successfully complete the task. This means that it is not as encapsulated as it could otherwise be, but I firmly believe that it is light-years ahead of parsing the command line a la K&R in The C Programming Language.
If you haven't done much with command-line parsing, this
article may seem like overkill, but I am convinced that this
is as close to the "right way" of doing things as I have seen.
I started parsing the command line based on the K&R
approach, modified it again when I found |