1/*
2 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package xmlkit; // -*- mode: java; indent-tabs-mode: nil -*-
26
27import java.util.*;
28/*
29 * @author jrose
30 */
31public class CommandLineParser {
32
33    public CommandLineParser(String optionString) {
34        setOptionMap(optionString);
35    }
36    TreeMap<String, String[]> optionMap;
37
38    public void setOptionMap(String options) {
39        // Convert options string into optLines dictionary.
40        TreeMap<String, String[]> optmap = new TreeMap<String, String[]>();
41        loadOptmap:
42        for (String optline : options.split("\n")) {
43            String[] words = optline.split("\\p{Space}+");
44            if (words.length == 0) {
45                continue loadOptmap;
46            }
47            String opt = words[0];
48            words[0] = "";  // initial word is not a spec
49            if (opt.length() == 0 && words.length >= 1) {
50                opt = words[1];  // initial "word" is empty due to leading ' '
51                words[1] = "";
52            }
53            if (opt.length() == 0) {
54                continue loadOptmap;
55            }
56            String[] prevWords = optmap.put(opt, words);
57            if (prevWords != null) {
58                throw new RuntimeException("duplicate option: "
59                        + optline.trim());
60            }
61        }
62        optionMap = optmap;
63    }
64
65    public String getOptionMap() {
66        TreeMap<String, String[]> optmap = optionMap;
67        StringBuffer sb = new StringBuffer();
68        for (String opt : optmap.keySet()) {
69            sb.append(opt);
70            for (String spec : optmap.get(opt)) {
71                sb.append(' ').append(spec);
72            }
73            sb.append('\n');
74        }
75        return sb.toString();
76    }
77
78    /**
79     * Remove a set of command-line options from args,
80     * storing them in the properties map in a canonicalized form.
81     */
82    public String parse(List<String> args, Map<String, String> properties) {
83        //System.out.println(args+" // "+properties);
84
85        String resultString = null;
86        TreeMap<String, String[]> optmap = optionMap;
87
88        // State machine for parsing a command line.
89        ListIterator<String> argp = args.listIterator();
90        ListIterator<String> pbp = new ArrayList<String>().listIterator();
91        doArgs:
92        for (;;) {
93            // One trip through this loop per argument.
94            // Multiple trips per option only if several options per argument.
95            String arg;
96            if (pbp.hasPrevious()) {
97                arg = pbp.previous();
98                pbp.remove();
99            } else if (argp.hasNext()) {
100                arg = argp.next();
101            } else {
102                // No more arguments at all.
103                break doArgs;
104            }
105            tryOpt:
106            for (int optlen = arg.length();; optlen--) {
107                // One time through this loop for each matching arg prefix.
108                String opt;
109                // Match some prefix of the argument to a key in optmap.
110                findOpt:
111                for (;;) {
112                    opt = arg.substring(0, optlen);
113                    if (optmap.containsKey(opt)) {
114                        break findOpt;
115                    }
116                    if (optlen == 0) {
117                        break tryOpt;
118                    }
119                    // Decide on a smaller prefix to search for.
120                    SortedMap<String, String[]> pfxmap = optmap.headMap(opt);
121                    // pfxmap.lastKey is no shorter than any prefix in optmap.
122                    int len = pfxmap.isEmpty() ? 0 : pfxmap.lastKey().length();
123                    optlen = Math.min(len, optlen - 1);
124                    opt = arg.substring(0, optlen);
125                    // (Note:  We could cut opt down to its common prefix with
126                    // pfxmap.lastKey, but that wouldn't save many cycles.)
127                }
128                opt = opt.intern();
129                assert (arg.startsWith(opt));
130                assert (opt.length() == optlen);
131                String val = arg.substring(optlen);  // arg == opt+val
132
133                // Execute the option processing specs for this opt.
134                // If no actions are taken, then look for a shorter prefix.
135                boolean didAction = false;
136                boolean isError = false;
137
138                int pbpMark = pbp.nextIndex();  // in case of backtracking
139                String[] specs = optmap.get(opt);
140                eachSpec:
141                for (String spec : specs) {
142                    if (spec.length() == 0) {
143                        continue eachSpec;
144                    }
145                    if (spec.startsWith("#")) {
146                        break eachSpec;
147                    }
148                    int sidx = 0;
149                    char specop = spec.charAt(sidx++);
150
151                    // Deal with '+'/'*' prefixes (spec conditions).
152                    boolean ok;
153                    switch (specop) {
154                        case '+':
155                            // + means we want an non-empty val suffix.
156                            ok = (val.length() != 0);
157                            specop = spec.charAt(sidx++);
158                            break;
159                        case '*':
160                            // * means we accept empty or non-empty
161                            ok = true;
162                            specop = spec.charAt(sidx++);
163                            break;
164                        default:
165                            // No condition prefix means we require an exact
166                            // match, as indicated by an empty val suffix.
167                            ok = (val.length() == 0);
168                            break;
169                    }
170                    if (!ok) {
171                        continue eachSpec;
172                    }
173
174                    String specarg = spec.substring(sidx);
175                    switch (specop) {
176                        case '.':  // terminate the option sequence
177                            resultString = (specarg.length() != 0) ? specarg.intern() : opt;
178                            break doArgs;
179                        case '?':  // abort the option sequence
180                            resultString = (specarg.length() != 0) ? specarg.intern() : arg;
181                            isError = true;
182                            break eachSpec;
183                        case '@':  // change the effective opt name
184                            opt = specarg.intern();
185                            break;
186                        case '>':  // shift remaining arg val to next arg
187                            pbp.add(specarg + val);  // push a new argument
188                            val = "";
189                            break;
190                        case '!':  // negation option
191                            String negopt = (specarg.length() != 0) ? specarg.intern() : opt;
192                            properties.remove(negopt);
193                            properties.put(negopt, null);  // leave placeholder
194                            didAction = true;
195                            break;
196                        case '$':  // normal "boolean" option
197                            String boolval;
198                            if (specarg.length() != 0) {
199                                // If there is a given spec token, store it.
200                                boolval = specarg;
201                            } else {
202                                String old = properties.get(opt);
203                                if (old == null || old.length() == 0) {
204                                    boolval = "1";
205                                } else {
206                                    // Increment any previous value as a numeral.
207                                    boolval = "" + (1 + Integer.parseInt(old));
208                                }
209                            }
210                            properties.put(opt, boolval);
211                            didAction = true;
212                            break;
213                        case '=':  // "string" option
214                        case '&':  // "collection" option
215                            // Read an option.
216                            boolean append = (specop == '&');
217                            String strval;
218                            if (pbp.hasPrevious()) {
219                                strval = pbp.previous();
220                                pbp.remove();
221                            } else if (argp.hasNext()) {
222                                strval = argp.next();
223                            } else {
224                                resultString = arg + " ?";
225                                isError = true;
226                                break eachSpec;
227                            }
228                            if (append) {
229                                String old = properties.get(opt);
230                                if (old != null) {
231                                    // Append new val to old with embedded delim.
232                                    String delim = specarg;
233                                    if (delim.length() == 0) {
234                                        delim = " ";
235                                    }
236                                    strval = old + specarg + strval;
237                                }
238                            }
239                            properties.put(opt, strval);
240                            didAction = true;
241                            break;
242                        default:
243                            throw new RuntimeException("bad spec for "
244                                    + opt + ": " + spec);
245                    }
246                }
247
248                // Done processing specs.
249                if (didAction && !isError) {
250                    continue doArgs;
251                }
252
253                // The specs should have done something, but did not.
254                while (pbp.nextIndex() > pbpMark) {
255                    // Remove anything pushed during these specs.
256                    pbp.previous();
257                    pbp.remove();
258                }
259
260                if (isError) {
261                    throw new IllegalArgumentException(resultString);
262                }
263
264                if (optlen == 0) {
265                    // We cannot try a shorter matching option.
266                    break tryOpt;
267                }
268            }
269
270            // If we come here, there was no matching option.
271            // So, push back the argument, and return to caller.
272            pbp.add(arg);
273            break doArgs;
274        }
275        // Report number of arguments consumed.
276        args.subList(0, argp.nextIndex()).clear();
277        // Report any unconsumed partial argument.
278        while (pbp.hasPrevious()) {
279            args.add(0, pbp.previous());
280        }
281        //System.out.println(args+" // "+properties+" -> "+resultString);
282        return resultString;
283    }
284}
285