Better documentation. Better scanner.
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Mon, 31 Mar 2025 22:13:34 +0000 (01:13 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Mon, 31 Mar 2025 22:13:34 +0000 (01:13 +0300)
src/main/java/eu/svjatoslav/commons/cli_helper/CLIHelper.java

index 9d574e0..96fdfab 100755 (executable)
@@ -7,51 +7,76 @@ package eu.svjatoslav.commons.cli_helper;
 import java.util.Scanner;
 
 /**
- * Command-line interface helper.
+ * <p>
+ * A utility class that provides methods for reading and validating various
+ * data types from the command line. The methods in this class prompt the user
+ * with a given message, read the user’s input, validate and parse that input,
+ * and then either return a valid value or re-prompt on invalid input.
+ * </p>
+ *
+ * <p>
+ * Each method optionally supports:
+ * </p>
+ * <ul>
+ *   <li>Returning a specified default value if the user presses ENTER without providing input.</li>
+ *   <li>Allowing empty input (returning <code>null</code>) when no default value is provided.</li>
+ *   <li>Enforcing minimum/maximum ranges or lengths (when applicable).</li>
+ * </ul>
+ *
+ * <p>
+ * Example usage:
+ * </p>
+ * <pre>
+ * {@code
+ *   Boolean answer = CLIHelper.askBoolean("Do you want to continue?", true);
+ *   Integer number = CLIHelper.askInteger("Enter a number between 5 and 10:", 7, 5, 10, false);
+ *   String name = CLIHelper.askString("What's your name?", "Anonymous", 2, 20, false);
+ * }
+ * </pre>
  */
 public class CLIHelper {
 
+    /**
+     * A single Scanner for all input reading, so we aren't creating new Scanners on
+     * {@link System#in} repeatedly.
+     */
+    private static final Scanner SCANNER = new Scanner(System.in);
+
     /**
      * Asks the user for a boolean value using the specified prompt on the command line.
-     * The user may respond with one of the following to indicate "true":
-     * <ul>
-     *   <li>y</li>
-     *   <li>yes</li>
-     *   <li>true</li>
-     * </ul>
-     * ...or one of the following to indicate "false":
+     * <p>
+     * Valid “true” responses are: <code>y</code>, <code>yes</code>, <code>true</code>.
+     * Valid “false” responses are: <code>n</code>, <code>no</code>, <code>false</code>.
+     * </p>
+     * <p>
+     * If the user presses ENTER (empty input):
+     * </p>
      * <ul>
-     *   <li>n</li>
-     *   <li>no</li>
-     *   <li>false</li>
+     *   <li>and {@code defaultValue} is non-null, return it;</li>
+     *   <li>otherwise if {@code allowEmpty} is true, return null;</li>
+     *   <li>otherwise keep prompting until valid input is given.</li>
      * </ul>
-     * If the user presses ENTER (empty input), the provided defaultValue is returned.
      *
      * @param prompt       the message to display to the user
-     * @param defaultValue the boolean value to return if the user provides no input. If null, user must provide input.
-     * @return {@code true} if the user answered affirmatively,
-     *         {@code false} if they answered negatively,
-     *         or {@code defaultValue} if input was empty
+     * @param defaultValue the boolean value to return if the user provides no input (may be {@code null})
+     * @param allowEmpty   if {@code true}, empty input is acceptable (and method returns {@code null} if no default); otherwise keep asking
+     * @return {@code true} if the user answered affirmatively, {@code false} if negatively, or the default/empty result as described above
      */
     public static Boolean askBoolean(final String prompt, final Boolean defaultValue, boolean allowEmpty) {
         while (true) {
-
             // If we have a defaultValue, display it in brackets; otherwise display no default
             String displayPrompt = prompt
                     + (defaultValue != null ? " [" + defaultValue + "]" : "")
                     + ": ";
 
-            // Read user input
             System.out.print(displayPrompt);
-            String line = new Scanner(System.in).nextLine().trim();
-            // If user just pressed Enter:
-            if (line.isEmpty()){
+            String line = SCANNER.nextLine().trim();
 
-                // If a defaultValue was supplied, return it
+            // If user just pressed Enter:
+            if (line.isEmpty()) {
                 if (defaultValue != null) {
                     return defaultValue;
                 }
-
                 if (allowEmpty) {
                     return null;
                 } else {
@@ -61,59 +86,71 @@ public class CLIHelper {
             }
 
             final String userInput = line.toLowerCase();
-
             if ("y".equals(userInput) || "yes".equals(userInput) || "true".equals(userInput)) {
                 return true;
             }
             if ("n".equals(userInput) || "no".equals(userInput) || "false".equals(userInput)) {
                 return false;
             }
-
             System.out.println("Invalid input. Please enter y/yes/true or n/no/false.");
         }
     }
 
+    /**
+     * Convenience method that calls {@link #askBoolean(String, Boolean, boolean)} with {@code allowEmpty = false}.
+     *
+     * @param prompt       the message to display to the user
+     * @param defaultValue the boolean value to return if the user provides no input (may be {@code null})
+     * @return {@code true} if the user answered affirmatively, {@code false} if negatively, or the default if provided
+     */
     public static Boolean askBoolean(String prompt, Boolean defaultValue) {
         return askBoolean(prompt, defaultValue, false);
     }
 
+    /**
+     * Convenience method that calls {@link #askBoolean(String, Boolean, boolean)} with no default and {@code allowEmpty = false}.
+     *
+     * @param prompt the message to display to the user
+     * @return {@code true} if the user answered affirmatively, {@code false} if negatively
+     * @throws IllegalStateException if the user never provides a valid input
+     */
     public static Boolean askBoolean(String prompt) {
         return askBoolean(prompt, null, false);
     }
 
     /**
      * Asks the user for a float value using the specified prompt on the command line.
-     * The user is prompted until a valid float (within optional ranges) is provided.
+     * <p>
+     * The user is prompted until a valid float (optionally enforced via {@code min}/{@code max})
+     * is provided. If {@code defaultValue} is specified and the user presses Enter, that default is
+     * returned. If {@code defaultValue} is {@code null} and the user presses Enter, the behavior
+     * depends on {@code allowEmpty}:
+     * </p>
      * <ul>
-     *   <li>If {@code defaultValue} is specified and user presses Enter, that default is returned.</li>
-     *   <li>If {@code defaultValue} is {@code null} and user presses Enter, returns {@code null}.</li>
-     *   <li>If {@code min} is not {@code null}, we enforce that the entered float is {@code >= min}.</li>
-     *   <li>If {@code max} is not {@code null}, we enforce that the entered float is {@code <= max}.</li>
+     *   <li>if {@code allowEmpty} is true, return {@code null};</li>
+     *   <li>otherwise, keep prompting until valid input is given.</li>
      * </ul>
      *
-     * @param prompt       The prompt displayed to the user
-     * @param defaultValue The default float if user simply presses Enter (may be null)
-     * @param min          The minimum acceptable value (inclusive), or null if no lower bound
-     * @param max          The maximum acceptable value (inclusive), or null if no upper bound
-     * @return A Float value that the user entered, or the defaultValue, or null if no defaultValue was given
+     * @param prompt       the prompt displayed to the user
+     * @param defaultValue the default float if user simply presses Enter (may be {@code null})
+     * @param min          the minimum acceptable value (inclusive), or {@code null} if no lower bound
+     * @param max          the maximum acceptable value (inclusive), or {@code null} if no upper bound
+     * @param allowEmpty   if {@code true}, empty input returns {@code null} when {@code defaultValue} is null
+     * @return a {@code Float} value entered by the user, or {@code defaultValue}, or {@code null}
      */
     public static Float askFloat(String prompt, Float defaultValue, Float min, Float max, boolean allowEmpty) {
         while (true) {
-            // If we have a defaultValue, display it in brackets; otherwise display no default
             String displayPrompt = prompt
                     + (defaultValue != null ? " [" + defaultValue + "]" : "")
                     + ": ";
 
-            // Read user input
             System.out.print(displayPrompt);
-            String input = new Scanner(System.in).nextLine().trim();
+            String input = SCANNER.nextLine().trim();
 
-            if (input.isEmpty()){
-                // If a defaultValue was supplied, return it
+            if (input.isEmpty()) {
                 if (defaultValue != null) {
                     return defaultValue;
                 }
-
                 if (allowEmpty) {
                     return null;
                 } else {
@@ -122,74 +159,70 @@ public class CLIHelper {
                 }
             }
 
-
-            // Parse float value
             try {
                 float parsedValue = Float.parseFloat(input);
-
-                // Check against min if specified
                 if (min != null && parsedValue < min) {
                     System.out.println("Value must be at least " + min + ".");
                     continue;
                 }
-
-                // Check against max if specified
                 if (max != null && parsedValue > max) {
                     System.out.println("Value must be at most " + max + ".");
                     continue;
                 }
-
-                // Parsed successfully within optional bounds
                 return parsedValue;
-
             } catch (NumberFormatException e) {
                 System.out.println("Invalid number format. Try again.");
             }
         }
     }
 
+    /**
+     * Convenience method for {@link #askFloat(String, Float, Float, Float, boolean)} with no min/max range and {@code allowEmpty = false}.
+     */
     public static Float askFloat(String prompt, Float defaultValue) {
         return askFloat(prompt, defaultValue, null, null, false);
     }
 
+    /**
+     * Convenience method for {@link #askFloat(String, Float, Float, Float, boolean)} with no defaultValue or min/max range and {@code allowEmpty = false}.
+     */
     public static Float askFloat(String prompt) {
         return askFloat(prompt, null, null, null, false);
     }
 
     /**
      * Asks the user for a long value using the specified prompt on the command line.
-     * The user is prompted until a valid long (within optional ranges) is provided.
+     * <p>
+     * The user is prompted until a valid long (optionally enforced via {@code min}/{@code max})
+     * is provided. If {@code defaultValue} is specified and the user presses Enter, that default is
+     * returned. If {@code defaultValue} is {@code null} and the user presses Enter, the behavior
+     * depends on {@code allowEmpty}:
+     * </p>
      * <ul>
-     *   <li>If {@code defaultValue} is specified and user presses Enter, that default is returned.</li>
-     *   <li>If {@code defaultValue} is {@code null} and user presses Enter, returns {@code null}.</li>
-     *   <li>If {@code min} is not {@code null}, we enforce that the entered long is {@code >= min}.</li>
-     *   <li>If {@code max} is not {@code null}, we enforce that the entered long is {@code <= max}.</li>
+     *   <li>if {@code allowEmpty} is true, return {@code null};</li>
+     *   <li>otherwise, keep prompting until valid input is given.</li>
      * </ul>
      *
-     * @param prompt       The prompt displayed to the user
-     * @param defaultValue The default long if user simply presses Enter (may be null)
-     * @param min          The minimum acceptable value (inclusive), or null if no lower bound
-     * @param max          The maximum acceptable value (inclusive), or null if no upper bound
-     * @return A Long value that the user entered, or the defaultValue, or null if no defaultValue was given
+     * @param prompt       the prompt displayed to the user
+     * @param defaultValue the default long if user simply presses Enter (may be {@code null})
+     * @param min          the minimum acceptable value (inclusive), or {@code null} if no lower bound
+     * @param max          the maximum acceptable value (inclusive), or {@code null} if no upper bound
+     * @param allowEmpty   if {@code true}, empty input returns {@code null} when {@code defaultValue} is null
+     * @return a {@code Long} value entered by the user, or {@code defaultValue}, or {@code null}
      */
     public static Long askLong(String prompt, Long defaultValue, Long min, Long max, boolean allowEmpty) {
         while (true) {
-            // If we have a defaultValue, display it in brackets; otherwise display no default
             String displayPrompt = prompt
                     + (defaultValue != null ? " [" + defaultValue + "]" : "")
                     + ": ";
 
-            // Read user input
             System.out.print(displayPrompt);
-            String input = new Scanner(System.in).nextLine().trim();
-
-            if (input.isEmpty()){
+            String input = SCANNER.nextLine().trim();
 
-                // If a defaultValue was supplied, return it
+            if (input.isEmpty()) {
                 if (defaultValue != null) {
                     return defaultValue;
                 }
-
                 if (allowEmpty) {
                     return null;
                 } else {
@@ -198,76 +231,70 @@ public class CLIHelper {
                 }
             }
 
-            // Parse long value
             try {
                 long parsedValue = Long.parseLong(input);
-
-                // Check against min if specified
                 if (min != null && parsedValue < min) {
                     System.out.println("Value must be at least " + min + ".");
                     continue;
                 }
-
-                // Check against max if specified
                 if (max != null && parsedValue > max) {
                     System.out.println("Value must be at most " + max + ".");
                     continue;
                 }
-
-                // Parsed successfully within optional bounds
                 return parsedValue;
-
             } catch (NumberFormatException e) {
                 System.out.println("Invalid number format. Try again.");
             }
         }
     }
 
+    /**
+     * Convenience method for {@link #askLong(String, Long, Long, Long, boolean)} with no min/max range and {@code allowEmpty = false}.
+     */
     public static Long askLong(String prompt, Long defaultValue) {
         return askLong(prompt, defaultValue, null, null, false);
     }
 
+    /**
+     * Convenience method for {@link #askLong(String, Long, Long, Long, boolean)} with no defaultValue or min/max range and {@code allowEmpty = false}.
+     */
     public static Long askLong(String prompt) {
         return askLong(prompt, null, null, null, false);
     }
 
-
     /**
      * Asks the user for an integer value using the specified prompt on the command line.
-     * The user is prompted until a valid integer (within optional ranges) is provided.
+     * <p>
+     * The user is prompted until a valid integer (optionally enforced via {@code min}/{@code max})
+     * is provided. If {@code defaultValue} is specified and the user presses Enter, that default is
+     * returned. If {@code defaultValue} is {@code null} and the user presses Enter, the behavior
+     * depends on {@code allowEmpty}:
+     * </p>
      * <ul>
-     *   <li>If {@code defaultValue} is specified and user presses Enter, that default is returned.</li>
-     *   <li>If {@code defaultValue} is {@code null} and user presses Enter, returns {@code null}.</li>
-     *   <li>If {@code min} is not {@code null}, we enforce that the entered integer is {@code >= min}.</li>
-     *   <li>If {@code max} is not {@code null}, we enforce that the entered integer is {@code <= max}.</li>
+     *   <li>if {@code allowEmpty} is true, return {@code null};</li>
+     *   <li>otherwise, keep prompting until valid input is given.</li>
      * </ul>
      *
-     * @param prompt       The prompt displayed to the user.
-     * @param defaultValue The default long if user simply presses Enter (may be null).
-     * @param min          The minimum acceptable value (inclusive), or null if no lower bound.
-     * @param max          The maximum acceptable value (inclusive), or null if no upper bound.
-     * @param allowEmpty   If <code>true</code>, empty input is acceptable. Otherwise keep asking user.
-     * @return An integer value that the user entered, or the defaultValue, or null if no defaultValue was given.
+     * @param prompt       the prompt displayed to the user
+     * @param defaultValue the default integer if the user simply presses Enter (may be {@code null})
+     * @param min          the minimum acceptable value (inclusive), or {@code null} if no lower bound
+     * @param max          the maximum acceptable value (inclusive), or {@code null} if no upper bound
+     * @param allowEmpty   if {@code true}, empty input returns {@code null} when {@code defaultValue} is null
+     * @return an {@code Integer} value that the user entered, or the {@code defaultValue}, or {@code null} if allowed
      */
     public static Integer askInteger(String prompt, Integer defaultValue, Integer min, Integer max, boolean allowEmpty) {
         while (true) {
-            // If we have a defaultValue, display it in brackets; otherwise display no default
             String displayPrompt = prompt
                     + (defaultValue != null ? " [" + defaultValue + "]" : "")
                     + ": ";
 
-            // Read user input
             System.out.print(displayPrompt);
-            String input = new Scanner(System.in).nextLine().trim();
-
-            // If user just pressed Enter:
-            if (input.isEmpty()){
+            String input = SCANNER.nextLine().trim();
 
-                // If a defaultValue was supplied, return it
+            if (input.isEmpty()) {
                 if (defaultValue != null) {
                     return defaultValue;
                 }
-
                 if (allowEmpty) {
                     return null;
                 } else {
@@ -276,65 +303,72 @@ public class CLIHelper {
                 }
             }
 
-            // Parse long value
             try {
                 int parsedValue = Integer.parseInt(input);
-
-                // Check against min if specified
                 if (min != null && parsedValue < min) {
                     System.out.println("Value must be at least " + min + ".");
                     continue;
                 }
-
-                // Check against max if specified
                 if (max != null && parsedValue > max) {
                     System.out.println("Value must be at most " + max + ".");
                     continue;
                 }
-
-                // Parsed successfully within optional bounds
                 return parsedValue;
-
             } catch (NumberFormatException e) {
                 System.out.println("Invalid number format. Try again.");
             }
         }
     }
 
+    /**
+     * Convenience method for {@link #askInteger(String, Integer, Integer, Integer, boolean)} with no min/max range and {@code allowEmpty = false}.
+     */
     public static Integer askInteger (String prompt, Integer defaultValue) {
         return askInteger(prompt, defaultValue, null, null, false);
     }
 
+    /**
+     * Convenience method for {@link #askInteger(String, Integer, Integer, Integer, boolean)} with no defaultValue or min/max range and {@code allowEmpty = false}.
+     */
     public static Integer askInteger (String prompt) {
         return askInteger(prompt, null, null, null, false);
     }
+
     /**
      * Asks the user for a string value using the specified prompt on the command line.
-     * If the user presses ENTER without typing anything and {@code defaultValue} is non-null,
-     * that default value is returned.
+     * <p>
+     * If the user presses ENTER without typing anything:
+     * </p>
+     * <ul>
+     *   <li>and {@code defaultValue} is non-null, return that default;</li>
+     *   <li>otherwise if {@code allowEmpty} is true, return {@code null};</li>
+     *   <li>otherwise, keep prompting until valid input is given.</li>
+     * </ul>
+     * <p>
+     * Additionally, if {@code minLength} or {@code maxLength} are specified, the input
+     * must fall within those character bounds.
+     * </p>
      *
      * @param prompt       the message to display to the user
-     * @param defaultValue the value to return if the user provides no input
-     * @return the value typed by the user, or {@code defaultValue} if empty input
+     * @param defaultValue the value to return if the user provides no input (may be {@code null})
+     * @param minLength    the minimum number of characters required, or {@code null} if no lower bound
+     * @param maxLength    the maximum number of characters allowed, or {@code null} if no upper bound
+     * @param allowEmpty   if {@code true}, empty input returns {@code null} when {@code defaultValue} is null
+     * @return the value typed by the user, {@code defaultValue}, or {@code null} as described above
      */
     public static String askString(String prompt, String defaultValue, Integer minLength, Integer maxLength, boolean allowEmpty) {
         while (true) {
-            // If we have a defaultValue, display it in brackets; otherwise display no default
             String displayPrompt = prompt
                     + (defaultValue != null ? " [" + defaultValue + "]" : "")
                     + ": ";
 
-            // Read user input
             System.out.print(displayPrompt);
-            String input = new Scanner(System.in).nextLine().trim();
+            String input = SCANNER.nextLine().trim();
 
-            if (input.isEmpty()){
-
-                // If a defaultValue was supplied, return it
+            if (input.isEmpty()) {
                 if (defaultValue != null) {
                     return defaultValue;
                 }
-
                 if (allowEmpty) {
                     return null;
                 } else {
@@ -347,7 +381,6 @@ public class CLIHelper {
                 System.out.println("Input must be at least " + minLength + " characters long.");
                 continue;
             }
-
             if (maxLength != null && input.length() > maxLength) {
                 System.out.println("Input must be at most " + maxLength + " characters long.");
                 continue;
@@ -357,12 +390,17 @@ public class CLIHelper {
         }
     }
 
+    /**
+     * Convenience method for {@link #askString(String, String, Integer, Integer, boolean)} with no min/max length and {@code allowEmpty = false}.
+     */
     public static String askString(String prompt, String defaultValue) {
         return askString(prompt, defaultValue, null, null, false);
     }
 
+    /**
+     * Convenience method for {@link #askString(String, String, Integer, Integer, boolean)} with no defaultValue or min/max length and {@code allowEmpty = false}.
+     */
     public static String askString(String prompt) {
         return askString(prompt, null, null, null, false);
     }
-
 }