001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.runtime; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022 023import java.util.*; 024 025import org.apache.juneau.commons.utils.*; 026 027/** 028 * Lean parser for command-line arguments passed in through a {@code main(String[] args)} method. 029 * 030 * <p> 031 * Supports the following grammars (all enabled by default; toggle via {@link Builder}): 032 * <ul> 033 * <li>Positional arguments — any token that does not start with a recognized flag prefix. 034 * <li>Short flags — <c>-key value</c> (and repeated <c>-key v1 -key v2</c> to collect multiple values). 035 * <li>Long flags — <c>--key value</c>. 036 * <li>Equals form — <c>--key=value</c> / <c>-key=value</c>. 037 * <li>System-property style — <c>-Dkey=value</c> (the leading {@code D} is stripped). 038 * </ul> 039 * 040 * <p> 041 * Positional arguments are only collected before the first flag is seen; once a flag appears, every subsequent 042 * non-flag token becomes a value of the most recently seen flag. This matches the legacy parsing rule and is 043 * preserved for backwards compatibility. 044 * 045 * <h5 class='section'>Example:</h5> 046 * <p class='bjava'> 047 * <jc>// Wrap raw command-line args.</jc> 048 * Args <jv>args</jv> = <jk>new</jk> Args(<jv>argv</jv>); 049 * 050 * <jc>// Positional access.</jc> 051 * <jv>args</jv>.get(0).ifPresent(System.<jsf>out</jsf>::println); 052 * 053 * <jc>// Named lookup with conversion via Optional.map(...).</jc> 054 * <jk>int</jk> <jv>port</jv> = <jv>args</jv>.get(<js>"port"</js>).map(Integer::parseInt).orElse(8080); 055 * 056 * <jc>// Multi-value flag.</jc> 057 * List<String> <jv>tags</jv> = <jv>args</jv>.getAll(<js>"tag"</js>); 058 * 059 * <jc>// Custom grammar.</jc> 060 * Args <jv>strict</jv> = Args.<jsm>create</jsm>().allowEquals(<jk>false</jk>).caseSensitive(<jk>false</jk>).build(<jv>argv</jv>); 061 * </p> 062 * 063 * <h5 class='section'>Notes:</h5><ul> 064 * <li class='note'>This class is immutable and thread-safe once constructed. 065 * <li class='note'>{@link #get(String)} returns the <i>first</i> value when a flag is repeated; use {@link #getAll(String)} for the full list. 066 * </ul> 067 */ 068public class Args { 069 070 private final List<String> positional; 071 private final Map<String,List<String>> options; 072 private final boolean caseSensitive; 073 074 /** 075 * Constructor that parses a raw command-line string using the default grammar. 076 * 077 * <p> 078 * The string is split on whitespace honouring single- and double-quoted segments via 079 * {@link StringUtils#splitQuoted(String)}. 080 * 081 * @param line The raw command line. Can be <jk>null</jk> (treated as an empty argv). 082 */ 083 public Args(String line) { 084 this(create().build(line)); 085 } 086 087 /** 088 * Constructor that parses an argv array using the default grammar. 089 * 090 * @param argv The arguments passed in through a <c>main(String[] args)</c> method. Can be <jk>null</jk> (treated as an empty argv). 091 */ 092 public Args(String[] argv) { 093 this(create().build(argv)); 094 } 095 096 private Args(Args copyFrom) { 097 this.positional = copyFrom.positional; 098 this.options = copyFrom.options; 099 this.caseSensitive = copyFrom.caseSensitive; 100 } 101 102 private Args(List<String> positional, Map<String,List<String>> options, boolean caseSensitive) { 103 this.positional = u(positional); 104 var copy = new LinkedHashMap<String,List<String>>(); 105 options.forEach((k,v) -> copy.put(k, u(v))); 106 this.options = u(copy); 107 this.caseSensitive = caseSensitive; 108 } 109 110 /** 111 * Creates a new builder for parsing arguments with non-default grammar. 112 * 113 * @return A new builder. 114 */ 115 public static Builder create() { 116 return new Builder(); 117 } 118 119 /** 120 * Returns the positional argument at the specified zero-based index. 121 * 122 * @param index The position. 123 * @return The argument value, or empty if {@code index} is out of range. 124 */ 125 public Optional<String> get(int index) { 126 if (index < 0 || index >= positional.size()) 127 return oe(); 128 return o(positional.get(index)); 129 } 130 131 /** 132 * Returns the first value of the named option, or empty if the option was not set. 133 * 134 * <p> 135 * If the option was specified multiple times (e.g. <c>-tag a -tag b</c>), this returns only the first value. 136 * Use {@link #getAll(String)} to get the full list. 137 * 138 * @param key The option name (without the flag prefix). 139 * @return The first value, or empty if the key is unset or has no values. 140 */ 141 public Optional<String> get(String key) { 142 var v = options.get(normalize(key)); 143 if (ie(v)) 144 return oe(); 145 return o(v.get(0)); 146 } 147 148 /** 149 * Returns all values for the named option in the order they appeared on the command line. 150 * 151 * @param key The option name (without the flag prefix). 152 * @return All values, or an empty list if the key is unset. 153 */ 154 public List<String> getAll(String key) { 155 var v = options.get(normalize(key)); 156 return v == null ? Collections.emptyList() : v; 157 } 158 159 /** 160 * Returns the positional arguments in the order they appeared on the command line. 161 * 162 * @return An unmodifiable list of positional arguments. 163 */ 164 public List<String> positional() { 165 return positional; 166 } 167 168 /** 169 * Returns the number of positional arguments. 170 * 171 * @return The count. 172 */ 173 public int argCount() { 174 return positional.size(); 175 } 176 177 /** 178 * Returns the number of distinct named options. 179 * 180 * @return The count. 181 */ 182 public int optionCount() { 183 return options.size(); 184 } 185 186 /** 187 * Returns <jk>true</jk> if no positional arguments and no named options were captured. 188 * 189 * @return <jk>true</jk> if both axes are empty. 190 */ 191 public boolean isEmpty() { 192 return positional.isEmpty() && options.isEmpty(); 193 } 194 195 /** 196 * Returns <jk>true</jk> if a positional argument exists at the given index. 197 * 198 * @param index The zero-based index. 199 * @return <jk>true</jk> if an argument exists at that index. 200 */ 201 public boolean has(int index) { 202 return index >= 0 && index < positional.size(); 203 } 204 205 /** 206 * Returns <jk>true</jk> if the named option appears in the parsed args (even if no values were supplied). 207 * 208 * @param key The option name. 209 * @return <jk>true</jk> if the option was set. 210 */ 211 public boolean has(String key) { 212 return options.containsKey(normalize(key)); 213 } 214 215 /** 216 * Returns the named-option contents as an unmodifiable map. 217 * 218 * <p> 219 * Positional arguments are <i>not</i> included — use {@link #positional()} for those. 220 * 221 * @return An unmodifiable map of named options to their value lists. 222 */ 223 public Map<String,List<String>> asMap() { 224 return options; 225 } 226 227 private String normalize(String key) { 228 return caseSensitive || key == null ? key : key.toLowerCase(Locale.ROOT); 229 } 230 231 /** 232 * Builder for {@link Args} with configurable grammar hooks. 233 * 234 * <p> 235 * Defaults match the legacy {@code -key value [value...]} parsing plus the natural extensions 236 * (<c>--key</c>, <c>--key=value</c>, <c>-Dkey=value</c>). Toggle individual options off to restrict the grammar. 237 */ 238 public static class Builder { 239 240 private boolean allowEquals = true; 241 private boolean allowShortFlags = true; 242 private boolean allowLongFlags = true; 243 private boolean allowSystemPropStyle = true; 244 private boolean caseSensitive = true; 245 private String[] customPrefix; 246 247 /** 248 * Enables or disables recognition of the <c>--key=value</c> / <c>-key=value</c> form. 249 * 250 * @param value <jk>true</jk> to recognize the equals form. Default <jk>true</jk>. 251 * @return This object. 252 */ 253 public Builder allowEquals(boolean value) { 254 allowEquals = value; 255 return this; 256 } 257 258 /** 259 * Enables or disables recognition of short-flag prefix <c>-</c>. 260 * 261 * @param value <jk>true</jk> to recognize <c>-key</c>-style flags. Default <jk>true</jk>. 262 * @return This object. 263 */ 264 public Builder allowShortFlags(boolean value) { 265 allowShortFlags = value; 266 return this; 267 } 268 269 /** 270 * Enables or disables recognition of long-flag prefix <c>--</c>. 271 * 272 * @param value <jk>true</jk> to recognize <c>--key</c>-style flags. Default <jk>true</jk>. 273 * @return This object. 274 */ 275 public Builder allowLongFlags(boolean value) { 276 allowLongFlags = value; 277 return this; 278 } 279 280 /** 281 * Enables or disables Java system-property style <c>-Dkey=value</c> parsing (strips the leading {@code D}). 282 * 283 * @param value <jk>true</jk> to recognize <c>-Dkey=value</c>. Default <jk>true</jk>. 284 * @return This object. 285 */ 286 public Builder allowSystemPropStyle(boolean value) { 287 allowSystemPropStyle = value; 288 return this; 289 } 290 291 /** 292 * Controls whether named-option lookups are case-sensitive. 293 * 294 * @param value <jk>true</jk> for case-sensitive lookups. Default <jk>true</jk>. 295 * @return This object. 296 */ 297 public Builder caseSensitive(boolean value) { 298 caseSensitive = value; 299 return this; 300 } 301 302 /** 303 * Overrides the default flag prefixes ({@code "-"} and {@code "--"}). 304 * 305 * <p> 306 * Pass an empty array to disable flag parsing entirely (every token becomes positional). When set, this 307 * supersedes {@link #allowShortFlags(boolean)} and {@link #allowLongFlags(boolean)}. 308 * 309 * @param prefixes The replacement prefix list, or <jk>null</jk> to keep the default behavior. 310 * @return This object. 311 */ 312 public Builder customPrefix(String...prefixes) { 313 customPrefix = prefixes; 314 return this; 315 } 316 317 /** 318 * Parses the given command-line string into an {@link Args} instance. 319 * 320 * @param line The raw command line. Can be <jk>null</jk> (treated as an empty argv). 321 * @return A new immutable {@link Args} instance. 322 */ 323 public Args build(String line) { 324 return build(line == null ? new String[0] : splitQuoted(line)); 325 } 326 327 /** 328 * Parses the given argv array into an {@link Args} instance. 329 * 330 * @param argv The arguments. Can be <jk>null</jk> (treated as an empty argv). 331 * @return A new immutable {@link Args} instance. 332 */ 333 @SuppressWarnings({ 334 "java:S135", // Argv tokenizer dispatch loop; per-token continue guards are clearer than restructuring the parse flow. 335 "java:S3776" // Cognitive complexity acceptable for argument prefix resolution and parsing logic 336 }) 337 public Args build(String[] argv) { 338 var prefixes = resolvePrefixes(); 339 var positional = new ArrayList<String>(); 340 var options = new LinkedHashMap<String,List<String>>(); 341 String currentKey = null; 342 343 if (argv != null) { 344 for (var token : argv) { 345 if (token == null) 346 continue; 347 var matched = matchPrefix(token, prefixes); 348 if (matched == null) { 349 if (currentKey != null) 350 options.get(currentKey).add(token); 351 else 352 positional.add(token); 353 continue; 354 } 355 var body = token.substring(matched.length()); 356 if (allowSystemPropStyle && body.length() > 1 && body.charAt(0) == 'D' && body.indexOf('=') >= 0) 357 body = body.substring(1); 358 if (allowEquals && body.indexOf('=') >= 0) { 359 var eq = body.indexOf('='); 360 var key = normalizeKey(body.substring(0, eq)); 361 var value = body.substring(eq + 1); 362 options.computeIfAbsent(key, k -> l()).add(value); 363 currentKey = null; 364 } else { 365 currentKey = normalizeKey(body); 366 options.computeIfAbsent(currentKey, k -> l()); 367 } 368 } 369 } 370 return new Args(positional, options, caseSensitive); 371 } 372 373 private List<String> resolvePrefixes() { 374 List<String> p; 375 if (customPrefix != null) { 376 p = list(customPrefix); 377 } else { 378 p = l(); 379 if (allowLongFlags) 380 p.add("--"); 381 if (allowShortFlags) 382 p.add("-"); 383 } 384 p.removeIf(Shorts::ie); 385 p.sort((a,b) -> Integer.compare(b.length(), a.length())); 386 return p; 387 } 388 389 private static String matchPrefix(String token, List<String> prefixes) { 390 for (var prefix : prefixes) { 391 if (token.length() > prefix.length() && token.startsWith(prefix)) 392 return prefix; 393 } 394 return null; 395 } 396 397 private String normalizeKey(String key) { 398 if (ine(key) && ! caseSensitive) 399 return key.toLowerCase(Locale.ROOT); 400 return key; 401 } 402 } 403}