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&lt;String&gt; <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}