001    /*--------------------------------------------------------------------------+
002    $Id: CommandLine.java 26283 2010-02-18 11:18:57Z juergens $
003    |                                                                          |
004    | Copyright 2005-2010 Technische Universitaet Muenchen                     |
005    |                                                                          |
006    | Licensed under the Apache License, Version 2.0 (the "License");          |
007    | you may not use this file except in compliance with the License.         |
008    | You may obtain a copy of the License at                                  |
009    |                                                                          |
010    |    http://www.apache.org/licenses/LICENSE-2.0                            |
011    |                                                                          |
012    | Unless required by applicable law or agreed to in writing, software      |
013    | distributed under the License is distributed on an "AS IS" BASIS,        |
014    | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
015    | See the License for the specific language governing permissions and      |
016    | limitations under the License.                                           |
017    +--------------------------------------------------------------------------*/
018    package edu.tum.cs.commons.options;
019    
020    import java.io.PrintWriter;
021    import java.util.ArrayList;
022    import java.util.Collections;
023    import java.util.List;
024    
025    import edu.tum.cs.commons.reflect.TypeConversionException;
026    import edu.tum.cs.commons.string.StringUtils;
027    
028    /**
029     * A class providing command line parsing and usage messages using GNU syntax.
030     * <p>
031     * The GNU syntax is implemented as follows. There are short (single character)
032     * and long (multi character) options, just as provided by the AOption
033     * annotation. Short options are introduced using a single minus (e.g. '-h')
034     * while long options are introduced using a double minus (e.g. '--help'). The
035     * parameter for an option is either the next argument, or--in case of long
036     * options--possibly separated by an equals sign (e.g. '--file=test.txt'). Short
037     * options may be chained (e.g. '-xvf abc' instead of '-x -v -f abc'). For
038     * chained short options, only the last option may take a parameter.
039     * 
040     * @author Benjamin Hummel
041     * @author $Author: juergens $
042     * 
043     * @version $Rev: 26283 $
044     * @levd.rating GREEN Hash: 11D10A0EF71752B4224881C9DF088659
045     */
046    public class CommandLine {
047    
048            /** Registry containing the options to be used by this instance */
049            private final OptionRegistry registry;
050    
051            /**
052             * Constructor.
053             * 
054             * @param registry
055             *            Registry containing the options to be used by this instance.
056             */
057            public CommandLine(OptionRegistry registry) {
058                    this.registry = registry;
059            }
060    
061            /**
062             * Parses the given command line parameters and applies the options found.
063             * The arguments not treated as options or parameters are returned (often
064             * they are treated as file arguments). If the syntax does not conform to
065             * the options in the registry, an {@link OptionException} is thrown.
066             * 
067             * @param args
068             *            the command line arguments to be parsed.
069             * @return the remaining arguments.
070             * @throws OptionException
071             *             in case of syntax errors or invalid parameters.
072             */
073            public String[] parse(String[] args) throws OptionException {
074                    return parse(new CommandLineTokenStream(args));
075            }
076    
077            /**
078             * Parses the command line parameters implicitly given by the token stream
079             * and applies the options found. The arguments not treated as options or
080             * parameters are returned (often they are treated as file arguments). If
081             * the syntax does not conform to the options in the registry an
082             * IllegalArgumentException is thrown.
083             * 
084             * @param ts
085             *            Token stream containing the arguments.
086             * @return Remaining arguments.
087             * @throws OptionException
088             *             in case of syntax errors or invalid parameters.
089             */
090            public String[] parse(CommandLineTokenStream ts) throws OptionException {
091                    List<String> fileArgs = new ArrayList<String>();
092    
093                    while (ts.hasNext()) {
094                            if (ts.nextIsLongOption()) {
095                                    String name = ts.nextLongOption();
096                                    OptionApplicator applicator = registry.getLongOption(name);
097                                    applyOption(applicator, formatLongOption(name), ts);
098                            } else if (ts.nextIsShortOption()) {
099                                    char name = ts.nextShortOption();
100                                    OptionApplicator applicator = registry.getShortOption(name);
101                                    applyOption(applicator, formatShortOption(name), ts);
102                            } else if (ts.nextIsFileArgument()) {
103                                    fileArgs.add(ts.next());
104                            } else {
105                                    throw new OptionException("Unexpected command line argument: "
106                                                    + ts.next());
107                            }
108                    }
109    
110                    String[] result = new String[fileArgs.size()];
111                    return fileArgs.toArray(result);
112            }
113    
114            /**
115             * Applies an option and tests for various errors.
116             * 
117             * @param applicator
118             *            the applicator for the option.
119             * @param optionName
120             *            the name of the option.
121             * @param ts
122             *            the token stream used to get additional parameters.
123             */
124            private void applyOption(OptionApplicator applicator, String optionName,
125                            CommandLineTokenStream ts) throws OptionException {
126                    if (applicator == null) {
127                            throw new OptionException("Unknown option: " + optionName);
128                    }
129                    if (applicator.requiresParameter()) {
130                            if (!ts.nextIsParameter()) {
131                                    throw new OptionException("Missing argument for option: "
132                                                    + optionName);
133                            }
134    
135                            do {
136                                    String parameter = ts.next();
137                                    try {
138                                            applicator.applyOption(parameter);
139                                    } catch (TypeConversionException e) {
140                                            throw new OptionException("Parameter " + parameter
141                                                            + " for option " + optionName
142                                                            + " is not of required type!");
143                                    }
144                            } while (applicator.isGreedy() && ts.hasNext()
145                                            && !(ts.nextIsLongOption() || ts.nextIsShortOption()));
146                    } else {
147                            applicator.applyOption();
148                    }
149            }
150    
151            /**
152             * Print the list of all supported options using reasonable default values
153             * for widths.
154             * 
155             * @param pw
156             *            the writer used for output.
157             */
158            public void printUsage(PrintWriter pw) {
159                    printUsage(pw, 20, 80);
160            }
161    
162            /**
163             * Print the list of all supported options.
164             * 
165             * @param pw
166             *            the writer to print to.
167             * @param firstCol
168             *            the width of the first column containing the option name
169             *            (without the trailing space).
170             * @param width
171             *            the maximal width of a line (aka terminal width).
172             */
173            public void printUsage(PrintWriter pw, int firstCol, int width) {
174                    List<AOption> sortedOptions = new ArrayList<AOption>(registry
175                                    .getAllOptions());
176                    Collections.sort(sortedOptions, new AOptionComparator());
177    
178                    for (AOption option : sortedOptions) {
179                            printOption(option, pw, firstCol, width);
180                    }
181                    pw.flush();
182            }
183    
184            /**
185             * Print a single option.
186             * 
187             * @param option
188             *            the option to be printed.
189             * @param pw
190             *            the writer to print to.
191             * @param firstCol
192             *            the width of the first column containing the option name
193             *            (without the trailing space).
194             * @param width
195             *            the maximal width of a line (aka terminal width).
196             */
197            private void printOption(AOption option, PrintWriter pw, int firstCol,
198                            int width) {
199                    String names = formatNames(option);
200                    pw.print(names);
201    
202                    // start new line (if name too long for firstCol) or indent correctly
203                    int pos = names.length();
204                    if (pos > firstCol) {
205                            pos = width + 1;
206                    } else {
207                            pw.print(StringUtils.fillString(firstCol - pos, ' '));
208                    }
209    
210                    // Format description using lines no longer than width
211                    String indent = StringUtils.fillString(firstCol, ' ');
212                    String[] words = option.description().split("\\s+");
213                    for (String word : words) {
214                            if (pos + 1 + word.length() > width) {
215                                    pw.println();
216                                    pw.print(indent);
217                                    pos = firstCol;
218                            }
219                            pw.print(' ');
220                            pw.print(word);
221                            pos += 1 + word.length();
222                    }
223                    pw.println();
224            }
225    
226            /**
227             * Format the names of an option for output.
228             * 
229             * @param option
230             *            the options to format.
231             * @return the formatted string.
232             */
233            private String formatNames(AOption option) {
234                    String names = "  ";
235                    if (option.shortName() == 0) {
236                            names += StringUtils.fillString(
237                                            2 + formatShortOption('x').length(), ' ');
238                    } else {
239                            names += formatShortOption(option.shortName());
240                            if (option.longName().length() > 0) {
241                                    names += ", ";
242                            }
243                    }
244                    if (option.longName().length() > 0) {
245                            names += formatLongOption(option.longName());
246                    }
247                    return names;
248            }
249    
250            /**
251             * Returns the user visible name for the given long option.
252             * 
253             * @param name
254             *            the name of the option to format.
255             * @return the user visible name for the given long option.
256             */
257            private String formatLongOption(String name) {
258                    return "--" + name;
259            }
260    
261            /**
262             * Returns the user visible name for the given short option.
263             * 
264             * @param name
265             *            the name of the option to format.
266             * @return the user visible name for the given short option.
267             */
268            private String formatShortOption(char name) {
269                    return "-" + name;
270            }
271    }