From d09fabda681102981ee352853fb6964486833c6a Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Wed, 14 May 2025 23:45:35 +0300 Subject: [PATCH] Initial commit --- .gitignore | 10 + COPYING | 121 ++++ doc/index.org | 195 ++++++ pom.xml | 125 ++++ .../commons/cli_helper/CLIHelper.java | 581 ++++++++++++++++++ .../cli_helper/parameter_parser/Option.java | 239 +++++++ .../parameter_parser/ParameterCount.java | 19 + .../cli_helper/parameter_parser/Parser.java | 140 +++++ .../parameter/DirectoryOption.java | 100 +++ .../parameter/DirectoryOptions.java | 92 +++ .../parameter/ExistenceType.java | 19 + .../parameter/FileOption.java | 118 ++++ .../parameter/FileOptions.java | 82 +++ .../parameter/FloatOption.java | 61 ++ .../parameter/IntegerOption.java | 61 ++ .../parameter/NullOption.java | 52 ++ .../parameter/StringOption.java | 75 +++ .../parameter/StringOptions.java | 46 ++ .../parameter_parser/ParserTest.java | 68 ++ tools/commit and push | 11 + tools/open with IntelliJ IDEA | 18 + tools/update web site | 29 + 22 files changed, 2262 insertions(+) create mode 100755 .gitignore create mode 100644 COPYING create mode 100644 doc/index.org create mode 100644 pom.xml create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/CLIHelper.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Option.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParameterCount.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Parser.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOption.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOptions.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/ExistenceType.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOption.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOptions.java create mode 100644 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FloatOption.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/IntegerOption.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/NullOption.java create mode 100755 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOption.java create mode 100644 src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOptions.java create mode 100755 src/test/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParserTest.java create mode 100755 tools/commit and push create mode 100755 tools/open with IntelliJ IDEA create mode 100755 tools/update web site diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0b5d65b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.settings/ +/.project +/.classpath +/target/ + +/doc/apidocs/ +/doc/index.html + +/.idea/ +/*.iml diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/doc/index.org b/doc/index.org new file mode 100644 index 0000000..85cda1a --- /dev/null +++ b/doc/index.org @@ -0,0 +1,195 @@ +:PROPERTIES: +:ID: bb4f96cd-458c-495b-a605-313b2e3e28d2 +:END: +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: CLI Helper - library to help implementing commandline interfaces +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +* Overview +:PROPERTIES: +:ID: fef7ebc3-0f00-4b82-a926-c0cfdf709762 +:END: +- See also: [[https://www3.svjatoslav.eu/projects/cli-helper/apidocs/][CLI Helper JavaDoc]] + +This is library intended to facilitate creation of commandline +applications in Java programming language. Library is packaged as an +artifact to Maven repository. This makes it simple to add library as +dependency to your project. + +Library provides following general functionalities: +- [[id:4fca35e4-fdf1-4675-a36f-6206d6fb72cb][Asking for user input]] +- [[id:eb7d5632-6152-4d37-8e55-1cf4da21c204][Commandline arguments processing]] + + +* User input helper +:PROPERTIES: +:ID: 4fca35e4-fdf1-4675-a36f-6206d6fb72cb +:END: + +The =CLIHelper= provides user-friendly methods to read different data +types from standard input. It helps validate user input, display +prompts, handle default values, and enforce optional constraints like +numeric ranges or string lengths. + +For quick usage, here’s a simple example showing how you might query +users for a boolean value, an integer, and a string: + +#+BEGIN_SRC java +import eu.svjatoslav.commons.cli_helper.CLIHelper; + +public class Demo { + public static void main(String[] args) { + Boolean proceed = CLIHelper.askBoolean("Do you want to proceed?", true); + Integer age = CLIHelper.askInteger("Please enter your age:", 18, 0, 120, false); + String name = CLIHelper.askString("What is your name?", "Anonymous"); + + System.out.println("Proceed: " + proceed); + System.out.println("Age: " + age); + System.out.println("Name: " + name); + } +} +#+END_SRC + +See [[https://www3.svjatoslav.eu/projects/cli-helper/apidocs/eu/svjatoslav/commons/cli_helper/CLIHelper.html][Javadoc]] for complete API reference. + +* CLI argument helper +:PROPERTIES: +:ID: eb7d5632-6152-4d37-8e55-1cf4da21c204 +:END: + +See also: [[https://clig.dev/][Command Line Interface Guidelines]]. + +** Command and argument + +Every command-line application has a way of receiving input from +users, usually in the form of command-line arguments. A command-line +argument is a piece of information provided to the command-line +application when it's invoked. These arguments are provided as an +array of strings. The first element of the array (argument 0) is +typically the name of the command itself. + +In the example below, 'my-video-coder' is our command, and the rest +are arguments: + +#+BEGIN_SRC shell +my-video-coder encode --input vid1.mp4 vid2.mp4 vid3.mp4 --quality 5 +#+END_SRC + +To better understand how these concepts work together, let's break +down our example command: + +| argument # | value(s) | type | +|------------+----------------------------+-------------------------------| +| 0 | my-video-coder | command | +| 1 | encode | [[id:94242e8a-c59b-42fd-8cc7-ba3df1938119][subcommand]] | +| 2 | --input | [[id:ffedf388-4d23-41eb-98d0-83fd3940b24d][option1]] | +| 3, 4, 5 | vid1.mp4 vid2.mp4 vid3.mp4 | [[id:8a39d20c-421f-4bc7-94e4-8e561e58bea0][parameters for --input option]] | +| 6 | --quality | [[id:ffedf388-4d23-41eb-98d0-83fd3940b24d][option2]] | +| 7 | 5 | [[id:8a39d20c-421f-4bc7-94e4-8e561e58bea0][parameter for --quaily option]] | + +** Subcommand +:PROPERTIES: +:ID: 94242e8a-c59b-42fd-8cc7-ba3df1938119 +:END: + +Subcommands are arguments that invoke more specific action that a +command can perform. They are often used with commands that have +multiple functions. In our example, *encode* is a subcommand of +*my-video-coder*. + +** Option +:PROPERTIES: +:ID: ffedf388-4d23-41eb-98d0-83fd3940b24d +:END: + +Options are arguments that change the behavior of a command or +subcommand. They usually start with a dash (-) or double dash +(--). For instance, *--input* and *--quality* are options in our +example command. + +** Parameter +:PROPERTIES: +:ID: 8a39d20c-421f-4bc7-94e4-8e561e58bea0 +:END: + +Parameter provides additional information to a command, subcommand or +option. + +For instance, in our example: +- 'vid1.mp4 vid2.mp4 vid3.mp4' are parameters for the *--input* option. +- '5' is a parameter for the *--quality* option. +* Getting the library +Follow instructions to embed *cli-helper* library in your project. + +Add following snippets to your project *pom.xml* file: + +#+BEGIN_SRC xml + + ... + + eu.svjatoslav + cli-helper + 1.2 + + ... + + + + + ... + + svjatoslav.eu + Svjatoslav repository + http://www3.svjatoslav.eu/maven/ + + ... + +#+END_SRC +* Getting the source code +- This program is free software: released under Creative Commons Zero + (CC0) license + +- Program author: + - Svjatoslav Agejenko + - Homepage: https://svjatoslav.eu + - Email: mailto://svjatoslav@svjatoslav.eu + +- [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] + +** Source code +- [[https://www2.svjatoslav.eu/gitweb/?p=cli-helper.git;a=snapshot;h=HEAD;sf=tgz][Download latest snapshot in TAR GZ format]] + +- [[https://www2.svjatoslav.eu/gitweb/?p=cli-helper.git;a=summary][Browse Git repository online]] + +- Clone Git repository using command: + : git clone https://www3.svjatoslav.eu/git/cli-helper.git + +- See [[https://www3.svjatoslav.eu/projects/cli-helper/apidocs/][JavaDoc]] +* TODO + +List of improvement suggestions: + +- Add more concrete examples of how to use the library in JavaDoc + comments. This will help developers quickly get started and learn + the API. + +- Provide more comprehensive unit tests for CliHelper, + ParameterParser, Options and subclasses. This will ensure robustness + and stability. + +- Add JavaDoc comments to all classes and methods where + applicable. This will provide better visibility into the library's + functionality for developers. + +- Add more option types like date/time, regular expression etc. + +- Document best practices for using the library in a larger project. + +- Implement support for more complex CLI applications like option + dependencies and conflicts resolution. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f9128a1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,125 @@ + + 4.0.0 + eu.svjatoslav + cli-helper + 1.3-SNAPSHOT + jar + CLI helper + Helper library for implementing commandline interface + http://www2.svjatoslav.eu/gitbrowse/cli-helper/doc/index.html + + + svjatoslav.eu + http://svjatoslav.eu + + + + UTF-8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 8 + 8 + UTF-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + + attach-javadocs + + jar + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.2 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.9.4 + + + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 2.6 + + + + + + + junit + junit + 4.13.2 + test + + + + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + + + scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/cli-helper.git + scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/cli-helper.git + HEAD + + + diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/CLIHelper.java b/src/main/java/eu/svjatoslav/commons/cli_helper/CLIHelper.java new file mode 100755 index 0000000..ae0ad34 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/CLIHelper.java @@ -0,0 +1,581 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under the Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper; + +import java.io.File; +import java.util.Scanner; + +/** + *

CLIHelper

+ *

+ * A collection of static convenience methods that simplify interactive command‑line + * applications by repeatedly prompting the user until valid input is obtained. + * Each {@code ask…} method supports optional default values, nullable results, + * value‑range enforcement, and context‑sensitive validation rules (e.g. file + * attributes). + *

+ * + *

Design highlights

+ * + * + *

Usage example

+ *
{@code
+ * Boolean proceed = CLIHelper.askBoolean("Continue?", true);
+ * Integer betweenFiveAndTen = CLIHelper.askInteger("Pick 5‑10", 7, 5, 10, false);
+ * String name = CLIHelper.askString("Your name", "Anonymous", 2, 20, false);
+ * }
+ */ +public final class CLIHelper { + + /** + * Shared {@link Scanner} for all input so that we do not open multiple + * scanners on {@link System#in System.in} (which would otherwise cause an + * {@code IllegalStateException} when one scanner closes the underlying + * stream). + */ + private static final Scanner SCANNER = new Scanner(System.in); + + /*‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ Boolean ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑*/ + + /** + * Repeatedly prompts the user for a Boolean value. + *

+ * Accepted affirmative tokens (case‑insensitive): y, yes, + * true.
+ * Accepted negative tokens: n, no, false. + *

+ * + *

If the user submits an empty line:

+ * + * + * @param prompt message shown to the user (without trailing colon) + * @param defaultValue value returned when the user simply hits {@code Enter}; may be {@code null} + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return {@code Boolean.TRUE}, {@code Boolean.FALSE}, the supplied {@code defaultValue}, or {@code null} + */ + public static Boolean askBoolean(final String prompt, final Boolean defaultValue, final boolean allowEmpty) { + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue + "]" : "") + ": "; + System.out.print(displayPrompt); + + String line = SCANNER.nextLine().trim(); + + if (line.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter y/yes/true or n/no/false."); + continue; + } + + switch (line.toLowerCase()) { + case "y": + case "yes": + case "true": + return Boolean.TRUE; + case "n": + case "no": + case "false": + return Boolean.FALSE; + default: + System.out.println("Invalid input. Please enter y/yes/true or n/no/false."); + } + } + } + + /** + * Convenience overload – assumes {@code allowEmpty == false}. + * + * @see #askBoolean(String, Boolean, boolean) + */ + public static Boolean askBoolean(final String prompt, final Boolean defaultValue) { + return askBoolean(prompt, defaultValue, false); + } + + /** + * Convenience overload – no default value, {@code allowEmpty == false}. + * + * @throws IllegalStateException if the user never provides a valid token + * @see #askBoolean(String, Boolean, boolean) + */ + public static Boolean askBoolean(final String prompt) { + return askBoolean(prompt, null, false); + } + + /*‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ Float ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑*/ + + /** + * Prompts the user for a {@code float} value that satisfies optional + * minimum/maximum bounds. + * + * @param prompt message shown to the user + * @param defaultValue value returned on empty input; may be {@code null} + * @param min inclusive lower bound, or {@code null} for no check + * @param max inclusive upper bound, or {@code null} for no check + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return the parsed {@link Float}, {@code defaultValue}, or {@code null} + */ + public static Float askFloat(final String prompt, final Float defaultValue, + final Float min, final Float max, final boolean allowEmpty) { + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue + "]" : "") + ": "; + System.out.print(displayPrompt); + String input = SCANNER.nextLine().trim(); + + if (input.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter a valid float."); + continue; + } + + try { + float parsed = Float.parseFloat(input); + if (min != null && parsed < min) { + System.out.println("Value must be at least " + min + "."); + continue; + } + if (max != null && parsed > max) { + System.out.println("Value must be at most " + max + "."); + continue; + } + return parsed; + } catch (NumberFormatException ex) { + System.out.println("Invalid number format. Try again."); + } + } + } + + /** + * Overload without bounds; {@code allowEmpty == false}. + * + * @see #askFloat(String, Float, Float, Float, boolean) + */ + public static Float askFloat(final String prompt, final Float defaultValue) { + return askFloat(prompt, defaultValue, null, null, false); + } + + /** + * Overload without default or bounds; {@code allowEmpty == false}. + * + * @see #askFloat(String, Float, Float, Float, boolean) + */ + public static Float askFloat(final String prompt) { + return askFloat(prompt, null, null, null, false); + } + + /*‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ Long ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑*/ + + /** + * Prompts the user for a {@code long} value that satisfies optional + * minimum/maximum bounds. + * + * @param prompt message shown to the user + * @param defaultValue value returned on empty input; may be {@code null} + * @param min inclusive lower bound, or {@code null} + * @param max inclusive upper bound, or {@code null} + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return the parsed {@link Long}, {@code defaultValue}, or {@code null} + */ + public static Long askLong(final String prompt, final Long defaultValue, + final Long min, final Long max, final boolean allowEmpty) { + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue + "]" : "") + ": "; + System.out.print(displayPrompt); + String input = SCANNER.nextLine().trim(); + + if (input.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter a valid long."); + continue; + } + + try { + long parsed = Long.parseLong(input); + if (min != null && parsed < min) { + System.out.println("Value must be at least " + min + "."); + continue; + } + if (max != null && parsed > max) { + System.out.println("Value must be at most " + max + "."); + continue; + } + return parsed; + } catch (NumberFormatException ex) { + System.out.println("Invalid number format. Try again."); + } + } + } + + /** + * Overload without bounds; {@code allowEmpty == false}. + */ + public static Long askLong(final String prompt, final Long defaultValue) { + return askLong(prompt, defaultValue, null, null, false); + } + + /** + * Overload without default or bounds; {@code allowEmpty == false}. + */ + public static Long askLong(final String prompt) { + return askLong(prompt, null, null, null, false); + } + + /*‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ Integer ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑*/ + + /** + * Prompts the user for an {@code int} value within optional bounds. + * + * @param prompt message shown to the user + * @param defaultValue value returned on empty input; may be {@code null} + * @param min inclusive lower bound, or {@code null} + * @param max inclusive upper bound, or {@code null} + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return the parsed {@link Integer}, {@code defaultValue}, or {@code null} + */ + public static Integer askInteger(final String prompt, final Integer defaultValue, + final Integer min, final Integer max, final boolean allowEmpty) { + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue + "]" : "") + ": "; + System.out.print(displayPrompt); + String input = SCANNER.nextLine().trim(); + + if (input.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter a valid integer."); + continue; + } + + try { + int parsed = Integer.parseInt(input); + if (min != null && parsed < min) { + System.out.println("Value must be at least " + min + "."); + continue; + } + if (max != null && parsed > max) { + System.out.println("Value must be at most " + max + "."); + continue; + } + return parsed; + } catch (NumberFormatException ex) { + System.out.println("Invalid number format. Try again."); + } + } + } + + /** + * Overload without bounds; {@code allowEmpty == false}. + */ + public static Integer askInteger(final String prompt, final Integer defaultValue) { + return askInteger(prompt, defaultValue, null, null, false); + } + + /** + * Overload without default or bounds; {@code allowEmpty == false}. + */ + public static Integer askInteger(final String prompt) { + return askInteger(prompt, null, null, null, false); + } + + /*‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ String ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑*/ + + /** + * Prompts the user for a non‑empty {@link String} and validates its length. + * + * @param prompt message shown to the user + * @param defaultValue value returned on empty input; may be {@code null} + * @param minLength inclusive lower bound; {@code null} means no check + * @param maxLength inclusive upper bound; {@code null} means no check + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return the typed {@link String}, {@code defaultValue}, or {@code null} + */ + public static String askString(final String prompt, final String defaultValue, + final Integer minLength, final Integer maxLength, final boolean allowEmpty) { + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue + "]" : "") + ": "; + System.out.print(displayPrompt); + String input = SCANNER.nextLine().trim(); + + if (input.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter a valid string."); + continue; + } + + if (minLength != null && input.length() < minLength) { + 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; + } + + return input; + } + } + + /** + * Overload without length bounds; {@code allowEmpty == false}. + */ + public static String askString(final String prompt, final String defaultValue) { + return askString(prompt, defaultValue, null, null, false); + } + + /** + * Overload without default or bounds; {@code allowEmpty == false}. + */ + public static String askString(final String prompt) { + return askString(prompt, null, null, null, false); + } + + /*‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ File / Directory ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑*/ + + /** + * Prompts the user for a file path and validates various + * attributes (existence, readability, writability, executability). + *

+ * A {@link File} object is returned as‑is; no attempt is made to + * canonicalise or resolve symlinks. + *

+ * + * @param prompt message shown to the user + * @param defaultValue value returned on empty input; may be {@code null} + * @param mustExist if non‑{@code null}: {@code true} ⇒ file must exist, {@code false} ⇒ file must not exist + * @param mustReadable if non‑{@code null}: {@code true} ⇒ {@link File#canRead()} must be {@code true} + * @param mustWritable if non‑{@code null}: {@code true} ⇒ {@link File#canWrite()} must be {@code true} + * @param mustExecutable if non‑{@code null}: {@code true} ⇒ {@link File#canExecute()} must be {@code true} + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return a {@link File} satisfying all constraints, {@code defaultValue}, or {@code null} + */ + public static File askFile(final String prompt, + final File defaultValue, + final Boolean mustExist, + final Boolean mustReadable, + final Boolean mustWritable, + final Boolean mustExecutable, + final boolean allowEmpty) { + + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue.getPath() + "]" : "") + ": "; + System.out.print(displayPrompt); + String input = SCANNER.nextLine().trim(); + + if (input.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter a file path."); + continue; + } + + File file = new File(input); + + /* Validation chain */ + if (file.exists() && !file.isFile()) { + System.out.println("Path is not a file. Please specify a file."); + continue; + } + + if (mustExist != null) { + if (mustExist && !file.exists()) { + System.out.println("File does not exist. You must specify an existing file."); + continue; + } else if (!mustExist && file.exists()) { + System.out.println("File already exists. You must specify a non‑existing file."); + continue; + } + } + + if (mustReadable != null) { + if (mustReadable && file.exists() && !file.canRead()) { + System.out.println("File is not readable. You must specify a readable file."); + continue; + } else if (!mustReadable && file.canRead()) { + System.out.println("File is readable. You must specify a non‑readable file."); + continue; + } + } + + if (mustWritable != null) { + if (mustWritable && file.exists() && !file.canWrite()) { + System.out.println("File is not writable. You must specify a writable file."); + continue; + } else if (!mustWritable && file.canWrite()) { + System.out.println("File is writable. You must specify a non‑writable file."); + continue; + } + } + + if (mustExecutable != null) { + if (mustExecutable && file.exists() && !file.canExecute()) { + System.out.println("File is not executable. You must specify an executable file."); + continue; + } else if (!mustExecutable && file.canExecute()) { + System.out.println("File is executable. You must specify a non‑executable file."); + continue; + } + } + + return file; + } + } + + /** + * Overload with no attribute constraints; {@code allowEmpty == false}. + */ + public static File askFile(final String prompt, final File defaultValue) { + return askFile(prompt, defaultValue, null, null, null, null, false); + } + + /** + * Overload with no default or attribute constraints; {@code allowEmpty == false}. + */ + public static File askFile(final String prompt) { + return askFile(prompt, null, null, null, null, null, false); + } + + /** + * Prompts the user for a directory path and validates various + * attributes (existence, readability, writability, executability). + * + * @param prompt message shown to the user + * @param defaultValue value returned on empty input; may be {@code null} + * @param mustExist if non‑{@code null}: {@code true} ⇒ directory must exist, {@code false} ⇒ directory must not exist + * @param mustReadable if non‑{@code null}: {@code true} ⇒ directory must be readable + * @param mustWritable if non‑{@code null}: {@code true} ⇒ directory must be writable + * @param mustExecutable if non‑{@code null}: {@code true} ⇒ directory must be executable + * @param allowEmpty whether an empty line without a default should yield {@code null} + * @return a directory {@link File} satisfying all constraints, {@code defaultValue}, or {@code null} + */ + public static File askDirectory(final String prompt, + final File defaultValue, + final Boolean mustExist, + final Boolean mustReadable, + final Boolean mustWritable, + final Boolean mustExecutable, + final boolean allowEmpty) { + + while (true) { + String displayPrompt = prompt + (defaultValue != null ? " [" + defaultValue.getPath() + "]" : "") + ": "; + System.out.print(displayPrompt); + String input = SCANNER.nextLine().trim(); + + if (input.isEmpty()) { + if (defaultValue != null) { + return defaultValue; + } + if (allowEmpty) { + return null; + } + System.out.println("Input cannot be empty. Please enter a directory path."); + continue; + } + + File dir = new File(input); + + if (dir.exists() && !dir.isDirectory()) { + System.out.println("Path is not a directory. Please specify a directory."); + continue; + } + + if (mustExecutable != null) { + if (mustExecutable && !dir.canExecute()) { + System.out.println("Directory is not executable. You must specify an executable directory."); + continue; + } else if (!mustExecutable && dir.canExecute()) { + System.out.println("Directory is executable. You must specify a non‑executable directory."); + continue; + } + } + + if (mustExist != null) { + if (mustExist && !dir.exists()) { + System.out.println("Directory does not exist. You must specify an existing directory."); + continue; + } else if (!mustExist && dir.exists()) { + System.out.println("Directory already exists. You must specify a non‑existing directory."); + continue; + } + } + + if (mustReadable != null) { + if (mustReadable && dir.exists() && !dir.canRead()) { + System.out.println("Directory is not readable. You must specify a readable directory."); + continue; + } else if (!mustReadable && dir.canRead()) { + System.out.println("Directory is readable. You must specify a non‑readable directory."); + continue; + } + } + + if (mustWritable != null) { + if (mustWritable && dir.exists() && !dir.canWrite()) { + System.out.println("Directory is not writable. You must specify a writable directory."); + continue; + } else if (!mustWritable && dir.canWrite()) { + System.out.println("Directory is writable. You must specify a non‑writable directory."); + continue; + } + } + + return dir; + } + } + + /** + * Overload with no attribute constraints; {@code allowEmpty == false}. + */ + public static File askDirectory(final String prompt, final File defaultValue) { + return askDirectory(prompt, defaultValue, null, null, null, null, false); + } + + /** + * Overload with no default or attribute constraints; {@code allowEmpty == false}. + */ + public static File askDirectory(final String prompt) { + return askDirectory(prompt, null, null, null, null, null, false); + } + +} diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Option.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Option.java new file mode 100755 index 0000000..eb33fa7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Option.java @@ -0,0 +1,239 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser; + +import java.util.ArrayList; +import java.util.List; + +import static eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount.NONE; +import static java.lang.String.join; +import static java.util.Collections.addAll; + +/** + * Represents a command-line option (a flag or switch), which can be configured with: + * + * + * @param the type of object returned by {@link #getValue()} + * @param the actual subclass type, used to enable fluent method chaining + */ +public abstract class Option> { + + /** + * Human-readable purpose of this option (e.g., "Input image path"). + * For a description of the parameter format (e.g., "file", "integer"), see {@link #describeFormat()}. + */ + public final String description; + + /** The list of parameters provided on the command line for this option (if any). */ + public final List parameters = new ArrayList<>(); + + /** How many parameters this option supports: NONE, ONE, or ONE_OR_MORE. */ + final ParameterCount parameterCount; + + /** The aliases by which this option can be invoked (e.g. "-f", "--file"). */ + private final List aliases = new ArrayList<>(); + + /** Whether this option is mandatory (i.e., must appear at least once on the CLI). */ + protected boolean mandatory = false; + + /** Whether this option was actually present on the CLI. */ + private boolean isPresent = false; + + /** + * Fully-custom constructor for an option. + * + * @param mandatory {@code true} if the option is required, {@code false} otherwise + * @param parameterCount the number of parameters required: NONE, ONE, or ONE_OR_MORE + * @param description a textual description of the option + * @param aliases2 zero or more aliases (e.g. "-f", "--file") + */ + public Option(final boolean mandatory, + final ParameterCount parameterCount, + final String description, + final String... aliases2) { + this.mandatory = mandatory; + this.description = description; + this.parameterCount = parameterCount; + addAll(aliases, aliases2); + } + + /** + * Simpler constructor that defaults to a non-mandatory option. + * You can make it mandatory later by calling {@link #setMandatory()}. + * + * @param description a textual description of the option + * @param parameterCount the number of parameters required: NONE, ONE, or ONE_OR_MORE + * @param aliases2 zero or more aliases + */ + public Option(final String description, + final ParameterCount parameterCount, + final String... aliases2) { + this(false, parameterCount, description, aliases2); + } + + /** + * Adds additional aliases to this option for user convenience. + * + * @param aliasArray additional aliases (e.g., "-f", "--file") + * @return this option (for method chaining) + */ + @SuppressWarnings("unchecked") + public I addAliases(final String... aliasArray) { + addAll(aliases, aliasArray); + return (I) this; + } + + /** + * Adds a parameter to this option, validating it against {@link #isValid(String)}. + * + * @param parameterString the parameter string to add + * @return {@code true} if the parameter was valid and added, otherwise {@code false} + */ + public boolean addParameter(final String parameterString) { + // Check if this option is supposed to have parameters at all + if (parameterCount.equals(NONE)) { + System.out.println("Error! No parameters are allowed for option(s): " + + getAliasesAsString()); + return false; + } + + // If only ONE parameter is allowed, but we already have one + if ((!parameters.isEmpty()) && parameterCount.equals(ParameterCount.ONE)) { + System.out.println("Error! Only one parameter is allowed for argument(s): " + + getAliasesAsString()); + return false; + } + + // Validate the parameter itself + if (isValid(parameterString)) { + parameters.add(parameterString); + return true; + } else { + System.out.println("Error! Invalid parameter \"" + parameterString + + "\". It should be " + describeFormat() + "."); + return false; + } + } + + /** + * Describes the kind or format of the expected parameter(s), e.g. "File", "Integer", etc. + * + * @return a short string describing the parameter format + */ + public abstract String describeFormat(); + + /** + * Returns this option's aliases as a comma-separated string (e.g. "-f, --file"). + */ + public String getAliasesAsString() { + return aliases.isEmpty() ? "" : join(", ", aliases); + } + + /** + * Returns a help message that includes the aliases, whether the option is mandatory, + * the format, and a brief description. + * + * @return a descriptive help string + */ + public String getHelp() { + StringBuilder result = new StringBuilder(); + // First line: aliases plus (mandatory?, format) + result.append(getAliasesAsString()); + if (!NONE.equals(parameterCount)) { + result.append(" (") + .append(isMandatory() ? "mandatory, " : "") + .append(describeFormat()) + .append(")"); + if (ParameterCount.ONE_OR_MORE.equals(parameterCount)) { + result.append("..."); + } + } + result.append("\n"); + // Second line: indentation plus description + result.append(" ").append(description).append("\n"); + return result.toString(); + } + + /** + * Returns the parsed value (usually constructed from {@link #parameters}). + * + * @return the parsed value as the generic type T. + * @throws RuntimeException if the parameters are insufficient or invalid in a subclass. + */ + public abstract T getValue(); + + /** + * @return {@code true} if this option is mandatory. + */ + public boolean isMandatory() { + return mandatory; + } + + /** + * @return {@code true} if this option was present on the command line. + */ + public boolean isPresent() { + return isPresent; + } + + /** + * Marks this option as present or not present. + * + * @param present {@code true} if it was found in the CLI arguments, else {@code false}. + */ + protected void setPresent(final boolean present) { + this.isPresent = present; + } + + /** + * Checks if a given alias matches this option. + * + * @param alias the alias to check + * @return {@code true} if the alias is registered for this option + */ + public boolean matchesAlias(final String alias) { + return aliases.contains(alias); + } + + /** + * Called when no more arguments can be added to this option, giving it a chance + * to verify correct usage (e.g., check if required parameters are missing). + * + * @return {@code true} if the usage so far is valid; otherwise {@code false}. + */ + public boolean noMoreArguments() { + // If we expect at least one parameter but none were provided + if (!parameterCount.equals(NONE) && parameters.isEmpty()) { + System.out.println("Error! " + getAliasesAsString() + + " requires at least one parameter."); + return false; + } + return true; + } + + /** + * Sets this option as mandatory. + * + * @return this option, for method chaining + */ + @SuppressWarnings("unchecked") + public I setMandatory() { + this.mandatory = true; + return (I) this; + } + + /** + * Checks if a single parameter string is valid for this option. + * + * @param value the parameter string to test + * @return {@code true} if valid; {@code false} otherwise + */ + public abstract boolean isValid(String value); +} diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParameterCount.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParameterCount.java new file mode 100755 index 0000000..1a30fe8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParameterCount.java @@ -0,0 +1,19 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser; + +/** + * Defines how many parameters a command-line option can accept. + */ +public enum ParameterCount { + /** Option has no parameters. */ + NONE, + + /** Option has exactly one parameter. */ + ONE, + + /** Option can have one or more parameters. */ + ONE_OR_MORE +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Parser.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Parser.java new file mode 100755 index 0000000..41c6b40 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Parser.java @@ -0,0 +1,140 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class parses command-line arguments against a set of registered {@link Option} objects. + *

+ * Usage: + *

{@code
+ *   Parser parser = new Parser();
+ *   FileOption fileOpt = parser.add(new FileOption("Input file").mustExist())
+ *                              .addAliases("-f", "--file")
+ *                              .setMandatory();
+ *   parser.parse(args);
+ *   if (!parser.checkSuccess()) {
+ *       parser.showHelp();
+ *       System.exit(1);
+ *   }
+ *   File inputFile = fileOpt.getValue();
+ * }
+ */ +public class Parser { + + private final List>> options = new ArrayList<>(); + + /** + * Registers an option with this parser. + * + * @param option the option to be added + * @param the concrete type of the option + * @return the same option object, for chaining + */ + public > E add(final E option) { + options.add(option); + return option; + } + + /** + * Looks up an option by alias. + * + * @param alias the alias to search for (e.g., "-f" or "--file") + * @return the matching option, or {@code null} if not found + */ + public Option findParameterByAlias(final String alias) { + for (final Option option : options) { + if (option.matchesAlias(alias)) { + return option; + } + } + return null; + } + + /** + * Parses the provided command-line arguments, matching them to registered options and their parameters. + * + * @param args command-line arguments (usually from main(String[])) + * @return {@code true} if parsing succeeded without errors; otherwise {@code false}. + */ + public boolean parse(final String[] args) { + Option currentOption = null; + + for (final String argument : args) { + Option optionForAlias = findParameterByAlias(argument); + + if (optionForAlias == null) { + // If this argument is not a recognized option alias, treat it as a parameter to the current option + if (currentOption == null) { + System.out.println("Unknown command-line parameter: " + argument); + return false; + } + // Attempt to add this argument as a parameter to the current option + if (!currentOption.addParameter(argument)) { + return false; + } + } else { + // We found a recognized option + // First, let the previous option finalize (check if it has all needed params, etc.) + if (currentOption != null) { + if (!currentOption.noMoreArguments()) { + return false; + } + } + // Switch to the newly recognized option + optionForAlias.setPresent(true); + currentOption = optionForAlias; + } + } + + // Finalize the last option (if any) + if (currentOption != null) { + if (!currentOption.noMoreArguments()) { + return false; + } + } + + // Finally, check mandatory options + return checkMandatoryArgumentsPresent(); + } + + /** + * Checks whether all mandatory options were present. + * + * @return {@code true} if all mandatory options are present, otherwise {@code false}. + */ + private boolean checkMandatoryArgumentsPresent() { + for (final Option option : options) { + if (option.isMandatory() && !option.isPresent()) { + System.out.println("Error! Mandatory parameter (" + + option.getAliasesAsString() + ") is not specified."); + return false; + } + } + return true; + } + + /** + * A convenience method to see if the last parse succeeded. If you stored the result of {@link #parse(String[])} in a + * boolean, you can also just check that. + * + * @return {@code true} if all mandatory options are present (and no parse errors occurred). + */ + public boolean checkSuccess() { + return checkMandatoryArgumentsPresent(); + } + + /** + * Prints all available options, their formats, and their descriptions. Usually called upon parse failure. + */ + public void showHelp() { + System.out.println("Available command-line arguments:"); + for (final Option option : options) { + System.out.print(option.getHelp()); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOption.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOption.java new file mode 100755 index 0000000..63c1714 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOption.java @@ -0,0 +1,100 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +import java.io.File; + +/** + * Represents a command-line option that accepts exactly one parameter interpreted as a directory path. + */ +public class DirectoryOption extends Option { + + /** Defines whether the directory must exist, must not exist, or if it does not matter. */ + private ExistenceType existenceType = ExistenceType.DOES_NOT_MATTER; + + /** + * Creates a DirectoryOption object with a specified description. + * + * @param description a brief description of what this directory option is for. + */ + public DirectoryOption(final String description) { + super(description, ParameterCount.ONE); + } + + @Override + public String describeFormat() { + switch (existenceType) { + case MUST_EXIST: return "Existing directory."; + case MUST_NOT_EXIST: return "Non-existing directory."; + default: return "Directory."; + } + } + + /** + * Returns the directory as a {@link File} object. + * + * @throws RuntimeException if the user did not provide exactly one parameter. + */ + @Override + public File getValue() { + if (parameters.size() != 1) { + throw new RuntimeException("Parameter '" + description + + "' must have exactly 1 argument."); + } + return new File(parameters.get(0)); + } + + /** + * Requires that the directory must already exist. + * + * @return this DirectoryOption instance for chaining + */ + public DirectoryOption mustExist() { + existenceType = ExistenceType.MUST_EXIST; + return this; + } + + /** + * Requires that the directory must not already exist. + * + * @return this DirectoryOption instance for chaining + */ + public DirectoryOption mustNotExist() { + existenceType = ExistenceType.MUST_NOT_EXIST; + return this; + } + + /** + * Checks whether the provided path is valid based on the {@link ExistenceType}. + * + * @param value the directory path to validate + * @return {@code true} if valid, otherwise {@code false} + */ + @Override + public boolean isValid(final String value) { + final File file = new File(value); + + switch (existenceType) { + case MUST_EXIST: + return file.exists() && file.isDirectory(); + + case MUST_NOT_EXIST: + return !file.exists(); + + case DOES_NOT_MATTER: + // If it exists and is a file, it's invalid + if (file.exists() && file.isFile()) { + return false; + } + return true; + + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOptions.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOptions.java new file mode 100755 index 0000000..fb0d923 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOptions.java @@ -0,0 +1,92 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +import java.io.File; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents a command-line option that accepts one or more directory paths. + */ +public class DirectoryOptions extends Option, DirectoryOptions> { + + /** Defines whether each directory must exist, must not exist, or if it does not matter. */ + private ExistenceType existenceType = ExistenceType.DOES_NOT_MATTER; + + /** + * Creates a DirectoryOptions object with a specified description, requiring one or more directories. + * + * @param description a brief description of what these directories represent. + */ + public DirectoryOptions(final String description) { + super(description, ParameterCount.ONE_OR_MORE); + } + + @Override + public String describeFormat() { + switch (existenceType) { + case MUST_EXIST: return "One to many existing directories."; + case MUST_NOT_EXIST: return "One to many non-existing directories."; + default: return "One to many directories."; + } + } + + /** + * Returns the directories as a list of {@link File} objects. + */ + @Override + public List getValue() { + return parameters.stream().map(File::new).collect(Collectors.toList()); + } + + /** + * Requires that each directory must exist. + * + * @return this DirectoryOptions instance + */ + public DirectoryOptions mustExist() { + existenceType = ExistenceType.MUST_EXIST; + return this; + } + + /** + * Requires that each directory must not exist. + * + * @return this DirectoryOptions instance + */ + public DirectoryOptions mustNotExist() { + existenceType = ExistenceType.MUST_NOT_EXIST; + return this; + } + + /** + * Validates each directory path against the specified {@link ExistenceType}. + */ + @Override + public boolean isValid(final String value) { + final File file = new File(value); + + switch (existenceType) { + case MUST_EXIST: + return file.exists() && file.isDirectory(); + + case MUST_NOT_EXIST: + return !file.exists(); + + case DOES_NOT_MATTER: + if (file.exists() && file.isFile()) { + return false; + } + return true; + + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/ExistenceType.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/ExistenceType.java new file mode 100755 index 0000000..a3cccc1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/ExistenceType.java @@ -0,0 +1,19 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +/** + * Defines whether a file/directory resource must exist, must not exist, or if it does not matter. + */ +public enum ExistenceType { + /** Resource shall exist. */ + MUST_EXIST, + + /** Resource shall not exist. */ + MUST_NOT_EXIST, + + /** Resource existence does not matter. */ + DOES_NOT_MATTER +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOption.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOption.java new file mode 100755 index 0000000..00c295d --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOption.java @@ -0,0 +1,118 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +import java.io.File; + +/** + * Represents a command-line option that accepts exactly one parameter which is interpreted as a file path. + * By default, {@link ExistenceType#DOES_NOT_MATTER} is used (i.e., the file may or may not exist). + */ +public class FileOption extends Option { + + /** Specifies whether the file must exist, must not exist, or does not matter. */ + private ExistenceType existenceType = ExistenceType.DOES_NOT_MATTER; + + /** + * Creates a FileOption requiring exactly one parameter that represents a file. + * + * @param description a brief description of what this option is for (e.g., "Path to input file"). + */ + public FileOption(final String description) { + super(description, ParameterCount.ONE); + } + + /** + * Checks whether the given file path is valid according to the specified {@link ExistenceType}. + * + * @param existenceType the required existence status for the file + * @param path the file path to validate + * @return {@code true} if the path meets the existence requirement and is not a directory when it must be a file. + */ + protected static boolean isFileValid(ExistenceType existenceType, String path) { + final File file = new File(path); + + switch (existenceType) { + case MUST_EXIST: + return file.exists() && file.isFile(); + + case MUST_NOT_EXIST: + return !file.exists(); + + case DOES_NOT_MATTER: + // If the file exists, ensure it's not a directory + if (file.exists() && file.isDirectory()) { + return false; + } + return true; + + default: + return false; + } + } + + /** + * Provides a short description of the expected format (e.g., "Existing file", "Non-existing file", or "File"). + * + * @return a string describing the format of this file parameter. + */ + @Override + public String describeFormat() { + switch (existenceType) { + case MUST_EXIST: return "Existing file."; + case MUST_NOT_EXIST: return "Non-existing file."; + default: return "File."; + } + } + + /** + * Returns the file chosen by the user (as a {@link File} object). + * + * @return a {@link File} object constructed from the single parameter. + * @throws RuntimeException if the option does not have exactly 1 parameter. + */ + @Override + public File getValue() { + if (parameters.size() != 1) { + throw new RuntimeException("Parameter '" + description + + "' must have exactly 1 argument."); + } + return new File(parameters.get(0)); + } + + /** + * Enforces that the file path must point to an existing file. + * + * @return this FileOption instance, for method chaining + */ + public FileOption mustExist() { + existenceType = ExistenceType.MUST_EXIST; + return this; + } + + /** + * Enforces that the file path must not exist. + * + * @return this FileOption instance, for method chaining + */ + public FileOption mustNotExist() { + existenceType = ExistenceType.MUST_NOT_EXIST; + return this; + } + + /** + * Validates the given parameter against the {@link ExistenceType} rule. + * + * @param value the file path to validate + * @return {@code true} if the file path meets the existence requirement + */ + @Override + public boolean isValid(final String value) { + return isFileValid(existenceType, value); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOptions.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOptions.java new file mode 100755 index 0000000..2d0e8ab --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOptions.java @@ -0,0 +1,82 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; + +import java.io.File; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents a command-line option that accepts one or more file paths. + */ +public class FileOptions extends Option, FileOptions> { + + /** Specifies whether each file must exist, must not exist, or does not matter. */ + private ExistenceType existenceType = ExistenceType.DOES_NOT_MATTER; + + /** + * Constructs a FileOptions object. + * + * @param description a brief description of the option's purpose. + */ + public FileOptions(final String description) { + super(description, ParameterCount.ONE_OR_MORE); + } + + /** + * Provides a short description of the expected file format(s). + * + * @return either "One to many existing files.", "One to many non-existing files.", or "One to many files." + */ + @Override + public String describeFormat() { + switch (existenceType) { + case MUST_EXIST: return "One to many existing files."; + case MUST_NOT_EXIST: return "One to many non-existing files."; + default: return "One to many files."; + } + } + + /** + * Returns the list of file paths as {@link File} objects. + * + * @return a list of {@link File} objects corresponding to user input. + */ + @Override + public List getValue() { + return parameters.stream().map(File::new).collect(Collectors.toList()); + } + + /** + * Requires that each file must already exist. + * + * @return this FileOptions instance for chaining + */ + public FileOptions mustExist() { + existenceType = ExistenceType.MUST_EXIST; + return this; + } + + /** + * Requires that each file must not already exist. + * + * @return this FileOptions instance for chaining + */ + public FileOptions mustNotExist() { + existenceType = ExistenceType.MUST_NOT_EXIST; + return this; + } + + /** + * Validates a single file path against the configured existence type. + */ + @Override + public boolean isValid(final String value) { + return FileOption.isFileValid(existenceType, value); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FloatOption.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FloatOption.java new file mode 100644 index 0000000..8aaca36 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FloatOption.java @@ -0,0 +1,61 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; + +/** + * Represents a command-line option that accepts exactly one floating-point parameter. + */ +public class FloatOption extends Option { + + /** + * Constructs a FloatOption with a brief description and exactly one parameter. + * + * @param description a brief description of the option (e.g., "Scale factor"). + */ + public FloatOption(final String description) { + super(description, ParameterCount.ONE); + } + + /** + * Describes the expected format ("Floating point number"). + */ + @Override + public String describeFormat() { + return "Floating point number. Example: 3.14"; + } + + /** + * Returns the float value specified by the user. + * + * @throws RuntimeException if the user did not provide exactly one parameter. + */ + @Override + public Float getValue() { + if (parameters.size() != 1) { + throw new RuntimeException("Parameter '" + description + + "' must have exactly 1 argument."); + } + return Float.parseFloat(parameters.get(0)); + } + + /** + * Checks if the given string can be parsed as a float. + * + * @param value the string to validate + * @return {@code true} if the string is a valid float, otherwise {@code false} + */ + @Override + public boolean isValid(final String value) { + try { + Float.valueOf(value); + return true; + } catch (final NumberFormatException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/IntegerOption.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/IntegerOption.java new file mode 100755 index 0000000..3fd6ead --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/IntegerOption.java @@ -0,0 +1,61 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +/** + * Represents a command-line option that accepts exactly one parameter interpreted as an integer. + */ +public class IntegerOption extends Option { + + /** + * Constructs an IntegerOption with exactly one parameter required. + * + * @param description a brief description of what this option represents. + */ + public IntegerOption(final String description) { + super(description, ParameterCount.ONE); + } + + /** + * Describes the expected format ("Integer."). + */ + @Override + public String describeFormat() { + return "Integer."; + } + + /** + * Returns the integer value specified by the user. + * + * @throws RuntimeException if the user did not provide exactly one parameter. + */ + @Override + public Integer getValue() { + if (parameters.size() != 1) { + throw new RuntimeException("Parameter '" + description + + "' must have exactly 1 argument."); + } + return Integer.parseInt(parameters.get(0)); + } + + /** + * Checks whether the given string can be parsed as an integer. + * + * @param value the string to validate + * @return {@code true} if the string is a valid integer, otherwise {@code false} + */ + @Override + public boolean isValid(final String value) { + try { + Integer.valueOf(value); + return true; + } catch (final NumberFormatException e) { + return false; + } + } +} diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/NullOption.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/NullOption.java new file mode 100755 index 0000000..cdf6583 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/NullOption.java @@ -0,0 +1,52 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +/** + * Represents a command-line option that accepts exactly zero parameters. + * Often used for flags that are either present or absent. + */ +public class NullOption extends Option { + + /** + * Creates a NullOption (e.g. a boolean flag) that requires no parameters. + * + * @param description a brief description of what this option does (e.g., "Enable debug mode"). + */ + public NullOption(final String description) { + super(description, ParameterCount.NONE); + } + + /** + * Describes the expected format of this option (i.e., no parameters). + * + * @return the string "None." + */ + @Override + public String describeFormat() { + return "None."; + } + + /** + * Returns whether this option was present on the command line. + * + * @return {@code true} if the user specified this option, otherwise {@code false}. + */ + @Override + public Boolean getValue() { + return isPresent(); + } + + /** + * Always returns {@code true}, since this option has no parameters and needs no validation. + */ + @Override + public boolean isValid(final String value) { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOption.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOption.java new file mode 100755 index 0000000..fa7f102 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOption.java @@ -0,0 +1,75 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +/** + * Represents a command-line option that accepts exactly one string parameter. + * An optional default value can be provided; if so, this option is considered "present" even without user input. + */ +public class StringOption extends Option { + + /** + * Default value to return if the user does not provide a parameter. + * If non-null, this option is automatically considered present. + */ + public final String defaultValue; + + /** + * Constructs a StringOption with no default value. + * + * @param description a brief description of what this option does. + */ + public StringOption(final String description) { + super(description, ParameterCount.ONE); + this.defaultValue = null; + } + + /** + * Constructs a StringOption with a specified default value. + * + * @param description a brief description of what this option does. + * @param defaultValue the default string to use if the user does not supply a parameter. + */ + public StringOption(final String description, String defaultValue) { + super(description, ParameterCount.ONE); + this.defaultValue = defaultValue; + + // If a default value is provided, mark this option as present by default. + this.setPresent(true); + } + + @Override + public String describeFormat() { + return "String."; + } + + /** + * Returns the string parameter or the default value if none was provided. + * + * @throws RuntimeException if multiple parameters were provided. + */ + @Override + public String getValue() { + if (parameters.isEmpty() && defaultValue != null) { + return defaultValue; + } + if (parameters.size() == 1) { + return parameters.get(0); + } + throw new RuntimeException("Parameter '" + description + + "' must have exactly 1 argument."); + } + + /** + * Always returns {@code true} since any string is considered valid for this option. + */ + @Override + public boolean isValid(final String value) { + return true; + } +} diff --git a/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOptions.java b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOptions.java new file mode 100644 index 0000000..59c0230 --- /dev/null +++ b/src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOptions.java @@ -0,0 +1,46 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser.parameter; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.ParameterCount; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Option; + +import java.util.List; + +/** + * Represents a command-line option that accepts one or more string parameters. + */ +public class StringOptions extends Option, StringOptions> { + + /** + * Creates a StringOptions object, requiring one or more string parameters. + * + * @param description a brief description of what these strings represent. + */ + public StringOptions(final String description) { + super(description, ParameterCount.ONE_OR_MORE); + } + + @Override + public String describeFormat() { + return "One to many string."; + } + + /** + * Returns the list of strings provided by the user. + */ + @Override + public List getValue() { + return parameters; + } + + /** + * Always returns true since any string is valid for this option. + */ + @Override + public boolean isValid(final String value) { + return true; + } +} \ No newline at end of file diff --git a/src/test/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParserTest.java b/src/test/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParserTest.java new file mode 100755 index 0000000..949e255 --- /dev/null +++ b/src/test/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParserTest.java @@ -0,0 +1,68 @@ +/* + * Svjatoslav Commons - shared library of common functionality. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.commons.cli_helper.parameter_parser; + +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.StringOption; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static org.junit.Assert.*; + +public class ParserTest { + + Parser parser; + + @Before + public void setUp() { + parser = new Parser(); + } + + @Test + public void testParse() throws IOException { + + // define allowed parameters + final StringOption helpOption = parser.add(new StringOption("Show help screen") + .addAliases("--help", "-h").setMandatory()); + + final StringOption compileOption = parser.add(new StringOption("Compile code")) + .addAliases("--compile", "-c"); + + FileOption fileOption = parser.add(new FileOption("Input file") + .addAliases("-i").mustExist()); + + createTemporaryFile(); + + // check help generation + parser.showHelp(); + + // parse arguments + parser.parse(new String[]{"--help", "section", "-i", "/tmp/file with spaces"}); + + // --help was in the arguments + assertTrue(helpOption.isPresent()); + + // compile was not present + assertFalse(compileOption.isPresent()); + + // validate that help argument was "section" + assertEquals("section", helpOption.getValue()); + + assertTrue(fileOption.isPresent()); + assertEquals("/tmp/file with spaces", fileOption.getValue().getAbsolutePath()); + + } + + private void createTemporaryFile() throws IOException { + File fileWithSpaces = new File("/tmp/file with spaces"); + FileWriter writer = new FileWriter(fileWithSpaces); + writer.write("test"); + writer.close(); + } +} diff --git a/tools/commit and push b/tools/commit and push new file mode 100755 index 0000000..057b511 --- /dev/null +++ b/tools/commit and push @@ -0,0 +1,11 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; + +cd .. + +cola +git push + +echo "" +echo "Press ENTER to close this window." +read diff --git a/tools/open with IntelliJ IDEA b/tools/open with IntelliJ IDEA new file mode 100755 index 0000000..de9bae5 --- /dev/null +++ b/tools/open with IntelliJ IDEA @@ -0,0 +1,18 @@ +#!/bin/bash + +# +# This is a helper bash script that starts IntelliJ with the current project. +# Script is written is such a way that you can simply click on it in file +# navigator to run it. +# +# +# Script assumes: +# +# + GNU operating system +# + IntelliJ is installed and commandline launcher "idea" is enabled. +# + +cd "${0%/*}" +cd .. + +setsid idea . &>/dev/null diff --git a/tools/update web site b/tools/update web site new file mode 100755 index 0000000..a630418 --- /dev/null +++ b/tools/update web site @@ -0,0 +1,29 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; + +cd .. + +# Build the project jar file and the apidocs. +mvn clean package + +# Export org to html using emacs in batch mode +( + cd doc/ + + rm -f index.html + emacs --batch -l ~/.emacs --visit=index.org --funcall=org-html-export-to-html --kill + + #rm setup.html + #emacs --batch -l ~/.emacs --visit=setup.org --funcall=org-html-export-to-html --kill +) + +# Copy the apidocs to the doc folder so that they can be uploaded to the server. +rm -rf doc/apidocs/ +cp -r target/apidocs/ doc/ + +# Upload project homepage to the server. +rsync -avz --delete -e 'ssh -p 10006' doc/ n0@www3.svjatoslav.eu:/mnt/big/projects/cli-helper/ + +echo "" +echo "Press ENTER to close this window." +read -- 2.20.1