001package org.unix4j.util;
002
003import java.util.ArrayList;
004import java.util.LinkedHashMap;
005import java.util.List;
006import java.util.Map;
007
008/**
009 * Provides static utility methods to parse options and operands of a command
010 * passed to the command as a string vararg parameter.
011 */
012public class ArgsUtil {
013
014        /**
015         * Returns a map with the options and operands. Operands are found in the
016         * returned map by operand name without the leading "--" prefix; the operand
017         * values are the values in the list. If operand values are provided without
018         * an operand name, they are returned in the map using the specified
019         * {@code defaultKey}.
020         * <p>
021         * Options are stored in the return map under the {@code optionsKey} with
022         * the option long or short names as values in the list. Option long names
023         * are added to the list without the leading "--" and short names without
024         * the leading single dash "-".
025         * <p>
026         * The argument "--" is accepted as a delimiter indicating the end of
027         * options and named operands. Any following arguments are treated as
028         * default operands returned in the map under the {@code defaultKey}, even
029         * if they begin with the '-' character.
030         * <p>
031         * String args that could be passed as arguments to the {@code echo} command, 
032         * assuming {@code optionsKey="options"} and {@code defaultKeys=["default"]}:
033         * 
034         * <pre>
035         * "--message" "hello" "world"        --> {"message":["hello", "world"]}
036         * "-n --message" "hello" "world"     --> {"message":["hello", "world"], "options":["n"]}
037         * "--noNewline --message" "ping"     --> {"message":["ping"], "options":["noNewline"]}
038         * "hello" "world"                    --> {"default":["hello", "world"]}
039         * "-n" "hello" "world"               --> {"default":["hello", "world"], "options":"n"}
040         * "--noNewline" "--" "hello" "world" --> {"default":["hello", "world"], "options":"noNewline"}
041         * "--" "8" "-" "7" "=" "1"           --> {"default":["8", "-", "7", "=", "1"}
042         * "--" "8" "--" "7" "=" "15"         --> {"default":["8", "--", "7", "=", "15"}
043         * </pre>
044         * <p>
045         * String args that could be passed as arguments to the {@code ls} command, 
046         * again with {@code optionsKey="options"} and {@code defaultKeys=["default"]}:
047         * 
048         * <pre>
049         * "-lart"                           --> {"options":["l", "a", "r", "t"]}
050         * "-laR" "--files" "*.txt" "*.log"  --> {"options":["l", "a", "R"], "files":["*.txt", "*.log"]}
051         * "-a" "--longFormat" "--files" "*" --> {"options":["a", "longFormat"], "files":["*"]}
052         * "-laR" "*.txt" "*.log"            --> {"options":["l", "a", "R"], "default":["*.txt", "*.log"]}
053         * "-la" "--" "-*" "--*"             --> {"options":["l", "a"], "default":["-*", "--*"]}
054         * </pre>
055         * <p>
056         * String args that could be passed as arguments to the {@code grep} command
057         * which has two default operands, hence {@code optionsKey="options"} and 
058         * {@code defaultKeys=["pattern","paths"]}:
059         * 
060         * <pre>
061         * "myword" "myfile.txt"                       --> {"pattern":["myword"], paths:["myfile.txt"]}
062         * "-i" "myword" "myfile.txt"                  --> {"options":["i"], "pattern":["myword"], paths:["myfile.txt"]}
063         * "-i" "error" "*.txt" "*.log"                --> {"options":["i"], "pattern":["error"], paths:["*.txt", "*.log"]}
064         * "--ignoreCase" "--" "error" "*"             --> {"options":["ignoreCase"], "pattern":["error"], paths:["*"]}
065         * "--ignoreCase" "--pattern" "error" "--" "*" --> {"options":["ignoreCase"], "pattern":["error"], paths:["*"]}
066         * "-i" "error" "--paths" "*"                  --> {"options":["i"], "pattern":["error"], paths:["*"]}
067         * "--ignoreCase" "--paths" "*" "--" "error"   --> {"options":["ignoreCase"], "pattern":["error"], paths:["*"]}
068         * </pre>
069         * 
070         * @param optionsKey
071         *            the map key to use for options aka no-value operands
072         * @param defaultKeys
073         *            a list of map keys to use for operands when no operand name is
074         *            specified; only the last key can have multiple operand values
075         * @param args
076         *            the arguments to be parsed
077         * @return the operands and options in a map with operand names as keys and
078         *         operand values as values plus the special key "options" with all
079         *         found option short/long names as values
080         */
081        public static final Map<String, List<Object>> parseArgs(String optionsKey, List<String> defaultKeys, Object... args) {
082                final Map<String, List<Object>> map = new LinkedHashMap<String, List<Object>>();
083                boolean allDefaultOperands = false;
084                String name = null;
085                List<Object> values = null;
086                for (int i = 0; i < args.length; i++) {
087                        final Object arg = args[i];
088                        if (allDefaultOperands) {
089                                final String defaultKey = getDefaultKey(map, defaultKeys);
090                                add(map, defaultKey, arg);
091                        } else {
092                                boolean isOperandValue = true;
093                                if (arg instanceof String) {
094                                        final String sarg = (String)arg;
095                                        if (sarg.startsWith("--")) {
096                                                isOperandValue = false;
097                                                add(optionsKey, map, name, values);
098                                                if (sarg.length() == 2) {
099                                                        // delimiter, all coming args are default operands
100                                                        allDefaultOperands = true;
101                                                        name = null;
102                                                        values = null;
103                                                } else {
104                                                        // operand name or option long name
105                                                        name = sarg.substring(2);// cut off the dashes --
106                                                        values = null;
107                                                }
108                                        } else if (sarg.startsWith("-") && !isDigit(sarg, 1)) {
109                                                isOperandValue = false;
110                                                // a short option name string
111                                                add(optionsKey, map, name, values);
112                                                final int len = sarg.length();
113                                                for (int j = 1; j < len; j++) {
114                                                        add(map, optionsKey, "" + sarg.charAt(j));
115                                                }
116                                                name = null;
117                                                values = null;
118                                        }
119                                }
120                                if (isOperandValue) {   
121                                        // an operand value
122                                        if (name == null) {
123                                                final String defaultKey = getDefaultKey(map, defaultKeys);
124                                                add(map, defaultKey, arg);
125                                        }
126                                        if (values == null) {
127                                                values = new ArrayList<Object>(2);
128                                        }
129                                        values.add(arg);
130                                }
131                        }
132                }
133                add(optionsKey, map, name, values);
134                return map;
135        }
136        
137        private static boolean isDigit(String s, int pos) {
138                return s.length() > pos && Character.isDigit(s.charAt(pos));
139        }
140
141        private static String getDefaultKey(Map<String, List<Object>> map, List<String> defaultKeys) {
142                for (final String defaultKey : defaultKeys) {
143                        if (!map.containsKey(defaultKey)) {
144                                return defaultKey;
145                        }
146                }
147                return defaultKeys.get(defaultKeys.size() - 1);
148        }
149
150        /**
151         * Adds the given value to the list in the map if it exist for the specified
152         * key, and to a new list added to the map if it does not exist yet
153         */
154        private static void add(Map<String, List<Object>> map, String key, Object value) {
155                List<Object> values = map.get(key);
156                if (values == null) {
157                        map.put(key, values = new ArrayList<Object>(2));
158                }
159                values.add(value);
160        }
161
162        /**
163         * If key is null, the method does nothing. Otherwise, if values is null,
164         * the key is an option name and therefore added to the map as an option. If
165         * values is not null, the key is an operand name and the values are added
166         * as operand values --- merged with existing operand values if there are
167         * any.
168         */
169        private static void add(String optionsKey, Map<String, List<Object>> map, String key, List<Object> values) {
170                if (key != null) {
171                        if (values == null) {
172                                // an option long name
173                                add(map, optionsKey, key);
174                        } else {
175                                // an operand
176                                List<Object> old = map.get(key);
177                                if (old == null) {
178                                        map.put(key, values);
179                                } else {
180                                        // merge
181                                        old.addAll(values);
182                                }
183                        }
184                }
185        }
186
187        // no instances
188        private ArgsUtil() {
189                super();
190        }
191}