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}