View Javadoc
1   package org.djutils.cli;
2   
3   import java.lang.reflect.Field;
4   import java.text.NumberFormat;
5   import java.util.Arrays;
6   import java.util.HashSet;
7   import java.util.LinkedHashMap;
8   import java.util.List;
9   import java.util.Locale;
10  import java.util.Map;
11  
12  import org.djutils.reflection.ClassUtil;
13  
14  import picocli.CommandLine;
15  import picocli.CommandLine.Command;
16  import picocli.CommandLine.Help;
17  import picocli.CommandLine.IHelpSectionRenderer;
18  import picocli.CommandLine.ITypeConverter;
19  import picocli.CommandLine.IVersionProvider;
20  import picocli.CommandLine.Model.ArgSpec;
21  import picocli.CommandLine.Option;
22  import picocli.CommandLine.ParseResult;
23  import picocli.CommandLine.Unmatched;
24  
25  /**
26   * CliUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:
27   * 
28   * <pre>
29   * public static void main(final String[] args) throws Exception
30   * {
31   *     Program program = new Program(); // initialize the Checkable class with the &#64;Option information
32   *     CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
33   *     // do rest of what the main method should do
34   * }
35   * </pre>
36   * 
37   * When the program is Checkable, the <code>check()</code> method is called after the arguments have been parsed. Here, further
38   * checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization
39   * of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive
40   * initialization is <b>not</b> carried out in the constructor of the program class that is given to the execute method.
41   * Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than
42   * the real program class. The real program can then take the values of the program from the Options class. An example:
43   * 
44   * <pre>
45   * public class Program
46   * {
47   *     &#64;Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
48   *     public static class Options implements Checkable
49   *     {
50   *         &#64;Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
51   *         private int port;
52   * 
53   *         public int getPort()
54   *         {
55   *             return this.port;
56   *         }
57   * 
58   *         &#64;Override
59   *         public void check() throws Exception
60   *         {
61   *             if (this.port &lt;= 0 || this.port &gt; 65535)
62   *                 throw new Exception("Port should be between 1 and 65535");
63   *         }
64   *     }
65   * 
66   *     public Program()
67   *     {
68   *         // initialization for the program; avoid really starting things
69   *     }
70   * 
71   *     public static void main(final String[] args)
72   *     {
73   *         Options options = new Options();
74   *         CliUtil.execute(options, args);
75   *         System.out.println("port = " + options.getPort());
76   *         // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
77   *     }
78   * }
79   * </pre>
80   * 
81   * <br>
82   * Copyright (c) 2019-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
83   * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
84   * source code and binary code of this software is proprietary information of Delft University of Technology.
85   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
86   */
87  public final class CliUtil
88  {
89      /** Mixin class to provide a --locale option, and a default locale. */
90      public static class InitLocale
91      {
92          /** the locale. */
93          @Option(names = {"--locale"}, defaultValue = "en-US", description = "locale for variables with units.")
94          private String locale;
95      }
96  
97      /** Retrieval class to provide a --locale option, and a default locale. */
98      public static class RetrieveLocale
99      {
100         /** the locale. */
101         @Option(names = {"--locale"}, defaultValue = "en-US", description = "locale for variables with units.")
102         private String locale;
103 
104         /**
105          * @return the locale from --locale
106          */
107         String getLocale()
108         {
109             return this.locale;
110         }
111 
112         /** The other options. */
113         @Unmatched
114         private List<String> remainder;
115     }
116 
117     /** Utility class constructor. */
118     private CliUtil()
119     {
120         // Utility class
121     }
122 
123     /**
124      * The map with overrides for default values and other Option and Program annotation values. values in the map are:
125      * <ul>
126      * <li>className%fieldName%propertyName for the &#64;Option annotation for field fieldName within the class named className,
127      * and the annotation property propertyName. An example of the propertyName is "defaultValue"</li>
128      * <li>className%propertyName for the &#64;Command annotation for the annotation property with propertyName in the named
129      * class. Examples of the propertyName are "name", "version", and "description"</li>
130      * </ul>
131      */
132     @SuppressWarnings("checkstyle:visibilitymodifier")
133     static Map<String, Object> overrideMap = new LinkedHashMap<>();
134 
135     /** The set locale for the last defaultValue to be parsed. */
136     @SuppressWarnings("checkstyle:visibilitymodifier")
137     private static Locale defaultValueLocale = Locale.US;
138 
139     /** The locale as set in the --locale option. */
140     @SuppressWarnings("checkstyle:visibilitymodifier")
141     private static Locale localeOption = null;
142 
143     /** Whether we are parsing a default value or not. */
144     @SuppressWarnings("checkstyle:visibilitymodifier")
145     private static boolean parseDefaultValue = false;
146     
147     /** Whether we are in test mode and should throw an exception instead of System.exit() with -v or -h. */
148     @SuppressWarnings("checkstyle:visibilitymodifier")
149     static boolean testMode = false;
150 
151     /**
152      * Parse the command line for the program. Register Unit converters, parse the command line, catch --help, --version and
153      * errors. If the program implements the Checkable interface, it calls the "check" method of the class that can take care of
154      * further checks of the CLI arguments. Potentially, check() can also provide other initialization of the program to be
155      * executed, but this can better be provided by other methods in main(). The method will exit on requesting help or version
156      * information, or when the arguments are not complete or not correct.
157      * @param program the potentially checkable program with the &#64;Option information
158      * @param args the arguments from the command line
159      */
160     public static void execute(final Object program, final String[] args)
161     {
162         execute(new CommandLine(program), args);
163     }
164 
165     /**
166      * Parse the given CommandLine object, that has been generated for a program. Register Unit converters, parse the command
167      * line, catch --help, --version, --locale, and errors. If the program implements the Checkable interface, it calls the
168      * "check" method of the class that can take care of further checks of the CLI arguments. Potentially, check() can also
169      * provide other initialization of the program to be executed, but this can better be provided by other methods in main().
170      * The method will exit on requesting help or version information, or when the arguments are not complete or not correct.
171      * @param commandLine the CommandLine object for the program with the &#64;Option information
172      * @param args the arguments from the command line
173      */
174     @SuppressWarnings("checkstyle:methodlength")
175     public static void execute(final CommandLine commandLine, final String[] args)
176     {
177         // Issue #13. add the --locale option
178         var initLocale = new InitLocale();
179         commandLine.addMixin("locale", initLocale);
180 
181         // set-up a new provider for default @Option values that can be overridden
182         CommandLine.IDefaultValueProvider vp = new CommandLine.IDefaultValueProvider()
183         {
184             @Override
185             public String defaultValue(final ArgSpec argSpec) throws Exception
186             {
187                 defaultValueLocale = Locale.US; // set to default if no annotation
188                 String fieldName = ((Field) argSpec.userObject()).getName();
189                 Class<?> fieldClass = null;
190                 try
191                 {
192                     Field field = ClassUtil.resolveField(commandLine.getCommand().getClass(), fieldName);
193                     fieldClass = field.getDeclaringClass();
194                     if (field.isAnnotationPresent(DefaultValueLocale.class))
195                     {
196                         var loc = field.getAnnotation(DefaultValueLocale.class).value();
197                         parseDefaultValue = true; // the next parse will be an annotated default value
198                         defaultValueLocale = parseLocale(loc);
199                     }
200                 }
201                 catch (NoSuchFieldException nsfe)
202                 {
203                     fieldClass = commandLine.getCommand().getClass();
204                 }
205                 String key = CliUtil.makeOverrideKeyProperty(fieldClass, fieldName, "defaultValue");
206                 if (CliUtil.overrideMap.containsKey(key))
207                 {
208                     return CliUtil.overrideMap.get(key).toString();
209                 }
210                 else
211                 {
212                     return argSpec.defaultValue();
213                 }
214             }
215         };
216         commandLine.setDefaultValueProvider(vp);
217 
218         // check @Program name override
219         String programKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "name");
220         if (overrideMap.containsKey(programKey))
221         {
222             commandLine.setCommandName(overrideMap.get(programKey).toString());
223         }
224 
225         // set-up the version provider that provides a version number that can be overridden
226         String versionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "version");
227         if (overrideMap.containsKey(versionKey))
228         {
229             commandLine.getCommandSpec().versionProvider(new IVersionProvider()
230             {
231                 @Override
232                 public String[] getVersion() throws Exception
233                 {
234                     if (overrideMap.get(versionKey) instanceof String[])
235                     {
236                         return (String[]) overrideMap.get(versionKey);
237                     }
238                     return new String[] {overrideMap.get(versionKey).toString()};
239                 }
240             });
241         }
242 
243         // set-up the description provider that provides a description that can be overridden
244         Map<String, IHelpSectionRenderer> helpMap = commandLine.getHelpSectionMap();
245         final IHelpSectionRenderer defaultDescriptionRenderer = helpMap.get("description");
246         helpMap.put("description", new IHelpSectionRenderer()
247         {
248             @Override
249             public String render(final Help help)
250             {
251                 String descriptionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "description");
252                 if (overrideMap.containsKey(descriptionKey))
253                 {
254                     if (overrideMap.get(descriptionKey) instanceof String[])
255                     {
256                         StringBuilder sb = new StringBuilder();
257                         for (String line : (String[]) overrideMap.get(descriptionKey))
258                         {
259                             sb.append(line);
260                             sb.append("\n");
261                         }
262                         return sb.toString();
263                     }
264                     return overrideMap.get(descriptionKey).toString();
265                 }
266                 return defaultDescriptionRenderer.render(help);
267             }
268         });
269         commandLine.setHelpSectionMap(helpMap);
270 
271         // register the DJUNITS converters
272         CliUnitConverters.registerAll(commandLine);
273         registerLocaleFloatConverters(commandLine);
274 
275         // Issue #13. set the locale, and store the old one
276         Locale saveLocale = Locale.getDefault();
277         RetrieveLocale retrieveLocale = new RetrieveLocale();
278         CommandLine cmdLocale = new CommandLine(retrieveLocale);
279         cmdLocale.parseArgs(args);
280         localeOption =
281                 new HashSet<String>(Arrays.asList(args)).contains("--locale") ? parseLocale(retrieveLocale.getLocale()) : null;
282 
283         // parse the command line arguments and handle errors, now based on the set locale
284         commandLine.getCommandSpec().parser().collectErrors(true);
285         ParseResult parseResult = commandLine.parseArgs(args);
286         List<Exception> parseErrors = parseResult.errors();
287         if (parseErrors.size() > 0)
288         {
289             for (Exception e : parseErrors)
290             {
291                 System.err.println(e.getMessage());
292             }
293             if (testMode)
294             {
295                 throw new CliRuntimeException("parse errors");
296             }
297             System.exit(-1);
298         }
299 
300         // process help and usage (using overridden values)
301         if (parseResult.isUsageHelpRequested())
302         {
303             commandLine.usage(System.out);
304             if (testMode)
305             {
306                 throw new CliRuntimeException("usage help requested");
307             }
308             System.exit(0);
309         }
310         else if (parseResult.isVersionHelpRequested())
311         {
312             commandLine.printVersionHelp(System.out);
313             if (testMode)
314             {
315                 throw new CliRuntimeException("version help requested");
316             }
317             System.exit(0);
318         }
319 
320         // check the values for the variables
321         Object program = commandLine.getCommand();
322         if (program instanceof Checkable)
323         {
324             try
325             {
326                 ((Checkable) program).check();
327             }
328             catch (Exception exception)
329             {
330                 System.err.println(exception.getMessage());
331                 if (testMode)
332                 {
333                     throw new CliRuntimeException("check error");
334                 }
335                 System.exit(-1);
336             }
337         }
338 
339         // Issue #13. reset the locale
340         Locale.setDefault(saveLocale);
341     }
342 
343     /**
344      * Change the value of a property of an already present &#64;Option annotation of a field in a class or superclass.
345      * @param programClass the class of the program for which the options should be changed
346      * @param fieldName the field for which the defaultValue in &#64;Option should be changed
347      * @param propertyName the name of the property to change the value of
348      * @param newValue the new value of the property
349      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
350      * @throws NoSuchFieldException when the field with the name does not exist in the program object
351      */
352     public static void changeOptionProperty(final Class<?> programClass, final String fieldName, final String propertyName,
353             final Object newValue) throws CliException, NoSuchFieldException
354     {
355         Field field = ClassUtil.resolveField(programClass, fieldName);
356         Option optionAnnotation = field.getAnnotation(Option.class);
357         if (optionAnnotation == null)
358         {
359             throw new CliException(
360                     String.format("@Option annotation not found for field %s in class %s", fieldName, programClass.getName()));
361         }
362         String key = makeOverrideKeyProperty(field.getDeclaringClass(), fieldName, propertyName);
363         overrideMap.put(key, newValue);
364     }
365 
366     /**
367      * Change the value of a property of an already present &#64;Option annotation of a field in a class or superclass.
368      * @param program the program for which the options should be changed
369      * @param fieldName the field for which the defaultValue in &#64;Option should be changed
370      * @param propertyName the name of the property to change the value of
371      * @param newValue the new value of the property
372      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
373      * @throws NoSuchFieldException when the field with the name does not exist in the program object
374      */
375     public static void changeOptionProperty(final Object program, final String fieldName, final String propertyName,
376             final Object newValue) throws CliException, NoSuchFieldException
377     {
378         changeOptionProperty(program.getClass(), fieldName, propertyName, newValue);
379     }
380 
381     /**
382      * Change the default value of an already present &#64;Option annotation of the "defaultValue" field in a class or
383      * superclass.
384      * @param program the program for which the options should be changed
385      * @param fieldName the field for which the defaultValue in &#64;Option should be changed
386      * @param newDefaultValue the new value of the defaultValue
387      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
388      * @throws NoSuchFieldException when the field with the name does not exist in the program object
389      */
390     public static void changeOptionDefault(final Object program, final String fieldName, final String newDefaultValue)
391             throws CliException, NoSuchFieldException
392     {
393         changeOptionProperty(program, fieldName, "defaultValue", newDefaultValue);
394     }
395 
396     /**
397      * Change the default value of an already present &#64;Option annotation of the "defaultValue" field in a class or
398      * superclass.
399      * @param programClass the class of the program for which the options should be changed
400      * @param fieldName the field for which the defaultValue in &#64;Option should be changed
401      * @param newDefaultValue the new value of the defaultValue
402      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
403      * @throws NoSuchFieldException when the field with the name does not exist in the program object
404      */
405     public static void changeOptionDefault(final Class<?> programClass, final String fieldName, final String newDefaultValue)
406             throws CliException, NoSuchFieldException
407     {
408         changeOptionProperty(programClass, fieldName, "defaultValue", newDefaultValue);
409     }
410 
411     /**
412      * Change the value of a property of an already present &#64;Command annotation in a class or superclass of that class.
413      * @param program the program for which the cli property should be changed
414      * @param propertyName the name of the property to change the value of
415      * @param newValue the new value of the property
416      * @throws CliException when the class is not annotated with &#64;Command
417      */
418     private static void changeCommandProperty(final Object program, final String propertyName, final Object newValue)
419             throws CliException
420     {
421         changeCommandProperty(program.getClass(), propertyName, newValue);
422     }
423 
424     /**
425      * Change the value of a property of an already present &#64;Command annotation in a class or superclass of that class.
426      * @param programClass the class of the program for which the options should be changed
427      * @param propertyName the name of the property to change the value of
428      * @param newValue the new value of the property
429      * @throws CliException when the class is not annotated with &#64;Command
430      */
431     private static void changeCommandProperty(final Class<?> programClass, final String propertyName, final Object newValue)
432             throws CliException
433     {
434         Class<?> declaringClass = getCommandAnnotationClass(programClass);
435         String key = makeOverrideKeyCommand(declaringClass, propertyName);
436         overrideMap.put(key, newValue);
437     }
438 
439     /**
440      * Change the value of the 'name' property of an already present &#64;Command annotation in a class or superclass of that
441      * class.
442      * @param program the program for which the cli property should be changed
443      * @param newName the new value of the name
444      * @throws CliException when the class is not annotated with &#64;Command
445      */
446     public static void changeCommandName(final Object program, final String newName) throws CliException
447     {
448         changeCommandProperty(program, "name", newName);
449     }
450 
451     /**
452      * Change the value of the 'name' property of an already present &#64;Command annotation in a class or superclass of that
453      * class.
454      * @param programClass the class of the program for which the options should be changed
455      * @param newName the new value of the name
456      * @throws CliException when the class is not annotated with &#64;Command
457      */
458     public static void changeCommandName(final Class<?> programClass, final String newName) throws CliException
459     {
460         changeCommandProperty(programClass, "name", newName);
461     }
462 
463     /**
464      * Change the value of the 'description' property of an already present &#64;Command annotation in a class or superclass of
465      * that class.
466      * @param program the program for which the cli property should be changed
467      * @param newDescription the new value of the description
468      * @throws CliException when the class is not annotated with &#64;Command
469      */
470     public static void changeCommandDescription(final Object program, final String newDescription) throws CliException
471     {
472         changeCommandProperty(program, "description", new String[] {newDescription});
473     }
474 
475     /**
476      * Change the value of the 'description' property of an already present &#64;Command annotation in a class or superclass of
477      * that class.
478      * @param programClass the class of the program for which the options should be changed
479      * @param newDescription the new value of the description
480      * @throws CliException when the class is not annotated with &#64;Command
481      */
482     public static void changeCommandDescription(final Class<?> programClass, final String newDescription) throws CliException
483     {
484         changeCommandProperty(programClass, "description", new String[] {newDescription});
485     }
486 
487     /**
488      * Change the value of the 'version' property of an already present &#64;Command annotation in a class or superclass of that
489      * class.
490      * @param program the program for which the cli property should be changed
491      * @param newVersion the new value of the version
492      * @throws CliException when the class is not annotated with &#64;Command
493      */
494     public static void changeCommandVersion(final Object program, final String newVersion) throws CliException
495     {
496         changeCommandProperty(program, "version", new String[] {newVersion});
497     }
498 
499     /**
500      * Change the value of the 'version' property of an already present &#64;Command annotation in a class or superclass of that
501      * class.
502      * @param programClass the class of the program for which the options should be changed
503      * @param newVersion the new value of the version
504      * @throws CliException when the class is not annotated with &#64;Command
505      */
506     public static void changeCommandVersion(final Class<?> programClass, final String newVersion) throws CliException
507     {
508         changeCommandProperty(programClass, "version", new String[] {newVersion});
509     }
510 
511     /**
512      * Return the &#64;Command annotation of a class or one of its superclasses.
513      * @param programClass the class of the program for which the annotation should be retrieved
514      * @return the &#64;Command annotation of the class or one of its superclasses
515      * @throws CliException when the class or one of its superclasses is not annotated with &#64;Command
516      */
517     public static Command getCommandAnnotation(final Class<?> programClass) throws CliException
518     {
519         return getCommandAnnotationClass(programClass).getDeclaredAnnotation(Command.class);
520     }
521 
522     /**
523      * Return the &#64;Command annotation of a class or one of its superclasses.
524      * @param programClass the class of the program for which the annotation should be retrieved
525      * @return the class or superclass in which the &#64;Command annotation was found
526      * @throws CliException when the class or one of its superclasses is not annotated with &#64;Command
527      */
528     public static Class<?> getCommandAnnotationClass(final Class<?> programClass) throws CliException
529     {
530         Class<?> clazz = programClass;
531         while (clazz != null)
532         {
533             Command commandAnnotation = clazz.getDeclaredAnnotation(Command.class);
534             if (commandAnnotation != null)
535             {
536                 return clazz;
537             }
538             clazz = clazz.getSuperclass();
539         }
540         throw new CliException(
541                 String.format("@Command annotation not found for class %s or one of its superclasses", programClass.getName()));
542     }
543 
544     /**
545      * @param programClass the class for which to retrieve the version. The class should be annotated with &#64;Command
546      * @return String[] the version string
547      * @throws CliException when the class is not annotated with &#64;Command
548      */
549     public static String[] getCommandVersion(final Class<?> programClass) throws CliException
550     {
551         String versionKey = makeOverrideKeyCommand(programClass, "version");
552         if (overrideMap.containsKey(versionKey))
553         {
554             Object version = overrideMap.get(versionKey);
555             if (version instanceof String[])
556             {
557                 return (String[]) version;
558             }
559             return new String[] {version.toString()};
560         }
561         return getCommandAnnotation(programClass).version();
562     }
563 
564     /**
565      * @param program the program for which to retrieve the version. The program's class should be annotated with &#64;Command
566      * @return String[] the version string
567      * @throws CliException when the class is not annotated with &#64;Command
568      */
569     public static String[] getCommandVersion(final Object program) throws CliException
570     {
571         return getCommandVersion(program.getClass());
572     }
573 
574     /**
575      * @param programClass the class for which to retrieve the program name. The class should be annotated with &#64;Command
576      * @return String the name string
577      * @throws CliException when the class is not annotated with &#64;Command
578      */
579     public static String getCommandName(final Class<?> programClass) throws CliException
580     {
581         String nameKey = makeOverrideKeyCommand(programClass, "name");
582         if (overrideMap.containsKey(nameKey))
583         {
584             return overrideMap.get(nameKey).toString();
585         }
586         return getCommandAnnotation(programClass).name();
587     }
588 
589     /**
590      * @param program the program for which to retrieve the program name. The program's class should be annotated with
591      *            &#64;Command
592      * @return String the name string
593      * @throws CliException when the class is not annotated with &#64;Command
594      */
595     public static String getCommandName(final Object program) throws CliException
596     {
597         return getCommandName(program.getClass());
598     }
599 
600     /**
601      * @param programClass the class for which to retrieve the description. The class should be annotated with &#64;Command
602      * @return String[] the description string
603      * @throws CliException when the class is not annotated with &#64;Command
604      */
605     public static String[] getCommandDescription(final Class<?> programClass) throws CliException
606     {
607         String descriptionKey = makeOverrideKeyCommand(programClass, "description");
608         if (overrideMap.containsKey(descriptionKey))
609         {
610             Object description = overrideMap.get(descriptionKey);
611             if (description instanceof String[])
612             {
613                 return (String[]) description;
614             }
615             return new String[] {description.toString()};
616         }
617         return getCommandAnnotation(programClass).description();
618     }
619 
620     /**
621      * @param program the program for which to retrieve the description. The program's class should be annotated with
622      *            &#64;Command
623      * @return String[] the description string
624      * @throws CliException when the class is not annotated with &#64;Command
625      */
626     public static String[] getCommandDescription(final Object program) throws CliException
627     {
628         return getCommandDescription(program.getClass());
629     }
630 
631     /**
632      * Make the override key for an option property.
633      * @param programClass the class of the program for which the options should be changed
634      * @param fieldName the field for which the defaultValue in &#64;Option should be changed
635      * @param propertyName the name of the property to change the value of
636      * @return the override key for an option property
637      */
638     static String makeOverrideKeyProperty(final Class<?> programClass, final String fieldName, final String propertyName)
639     {
640         return programClass.getName() + "%" + fieldName + "%" + propertyName;
641     }
642 
643     /**
644      * Make the override key for the Command annotation.
645      * @param programClass the class of the program for which the options should be changed
646      * @param propertyName the name of the annotation property to change the value of
647      * @return the override key for an option property
648      */
649     static String makeOverrideKeyCommand(final Class<?> programClass, final String propertyName)
650     {
651         return programClass.getName() + "%" + propertyName;
652     }
653 
654     /**
655      * Register the locale-dependent converters for double, Double, float, Float.
656      * @param cmd the CommandLine for which the DJUNITS converters should be registered
657      */
658     static void registerLocaleFloatConverters(final CommandLine cmd)
659     {
660         cmd.registerConverter(Double.class, new DoubleConverter());
661         cmd.registerConverter(double.class, new DoubleConverter());
662         cmd.registerConverter(Float.class, new FloatConverter());
663         cmd.registerConverter(float.class, new FloatConverter());
664     }
665 
666     /**
667      * Convert a Double String on the command line to a Double, taking into account the locale.
668      */
669     public static class DoubleConverter implements ITypeConverter<Double>
670     {
671         @Override
672         public Double convert(final String value) throws Exception
673         {
674             prepareLocale();
675             NumberFormat format = NumberFormat.getInstance(Locale.getDefault());
676             Number number;
677             double ret;
678             try
679             {
680                 number = format.parse(value);
681                 ret = number.doubleValue();
682             }
683             catch (Exception e)
684             {
685                 System.err.println("ERROR parsing double value " + value + ": " + e.getMessage());
686                 ret = Double.NaN;
687             }
688             restoreLocale();
689             return ret;
690         }
691     }
692 
693     /**
694      * Convert a Double String on the command line to a Double, taking into account the locale.
695      */
696     public static class FloatConverter implements ITypeConverter<Float>
697     {
698         @Override
699         public Float convert(final String value) throws Exception
700         {
701             prepareLocale();
702             NumberFormat format = NumberFormat.getInstance(Locale.getDefault());
703             Number number;
704             float ret;
705             try
706             {
707                 number = format.parse(value);
708                 ret = number.floatValue();
709             }
710             catch (Exception e)
711             {
712                 System.err.println("ERROR parsing double value " + value + ": " + e.getMessage());
713                 ret = Float.NaN;
714             }
715             restoreLocale();
716             return ret;
717         }
718     }
719 
720     /** temporarily save the locale. */
721     private static Locale saveLocaleForDefault;
722 
723     /**
724      * Prepare the setting of a Locale for a default value or a value.
725      */
726     static void prepareLocale()
727     {
728         saveLocaleForDefault = Locale.getDefault();
729         Locale.setDefault(
730                 CliUtil.localeOption != null && !CliUtil.parseDefaultValue ? CliUtil.localeOption : CliUtil.defaultValueLocale);
731     }
732 
733     /**
734      * Restore the setting of a Locale for a default value or a value.
735      */
736     static void restoreLocale()
737     {
738         Locale.setDefault(saveLocaleForDefault);
739         CliUtil.defaultValueLocale = Locale.US;
740         CliUtil.parseDefaultValue = false;
741     }
742 
743     /**
744      * Parse a locale string like "en-US" or "en_GB" or "nl" or "nl_NL".
745      * @param localeStr the string to parse
746      * @return the Locale belonging to the string
747      */
748     private static Locale parseLocale(final String localeStr)
749     {
750         var s = localeStr.replaceAll("_", "-");
751         var sa = s.split("\\-", 3);
752         return sa.length == 3 ? new Locale(sa[0], sa[1], sa[2])
753                 : sa.length == 2 ? new Locale(sa[0], sa[1]) : sa.length == 1 ? new Locale(sa[0]) : new Locale(s);
754     }
755 
756 }