Initial commit
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 14 May 2025 20:45:35 +0000 (23:45 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 14 May 2025 20:45:35 +0000 (23:45 +0300)
22 files changed:
.gitignore [new file with mode: 0755]
COPYING [new file with mode: 0644]
doc/index.org [new file with mode: 0644]
pom.xml [new file with mode: 0644]
src/main/java/eu/svjatoslav/commons/cli_helper/CLIHelper.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Option.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParameterCount.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/Parser.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOption.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/DirectoryOptions.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/ExistenceType.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOption.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FileOptions.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/FloatOption.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/IntegerOption.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/NullOption.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOption.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/commons/cli_helper/parameter_parser/parameter/StringOptions.java [new file with mode: 0644]
src/test/java/eu/svjatoslav/commons/cli_helper/parameter_parser/ParserTest.java [new file with mode: 0755]
tools/commit and push [new file with mode: 0755]
tools/open with IntelliJ IDEA [new file with mode: 0755]
tools/update web site [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100755 (executable)
index 0000000..0b5d65b
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..85cda1a
--- /dev/null
@@ -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
+<dependencies>
+    ...
+    <dependency>
+        <groupId>eu.svjatoslav</groupId>
+        <artifactId>cli-helper</artifactId>
+        <version>1.2</version>
+    </dependency>
+    ...
+</dependencies>
+
+
+<repositories>
+    ...
+    <repository>
+        <id>svjatoslav.eu</id>
+        <name>Svjatoslav repository</name>
+        <url>http://www3.svjatoslav.eu/maven/</url>
+    </repository>
+    ...
+</repositories>
+#+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 (file)
index 0000000..f9128a1
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,125 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>eu.svjatoslav</groupId>
+    <artifactId>cli-helper</artifactId>
+    <version>1.3-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>CLI helper</name>
+    <description>Helper library for implementing commandline interface</description>
+    <url>http://www2.svjatoslav.eu/gitbrowse/cli-helper/doc/index.html</url>
+
+    <organization>
+        <name>svjatoslav.eu</name>
+        <url>http://svjatoslav.eu</url>
+    </organization>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>8</source>
+                    <target>8</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.2.1</version>
+                <executions>
+                    <execution>
+                        <id>attach-sources</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.5.0</version>
+                <executions>
+                    <execution>
+                        <id>attach-javadocs</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+<!--                <configuration>-->
+<!--                    &lt;!&ndash; workaround for https://bugs.openjdk.java.net/browse/JDK-8212233 &ndash;&gt;-->
+<!--                    <javaApiLinks>-->
+<!--                        <property>-->
+<!--                            <name>foo</name>-->
+<!--                            <value>bar</value>-->
+<!--                        </property>-->
+<!--                    </javaApiLinks>-->
+<!--                    &lt;!&ndash; Workaround for https://stackoverflow.com/questions/49472783/maven-is-unable-to-find-javadoc-command &ndash;&gt;-->
+<!--                    <javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>-->
+<!--                </configuration>-->
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-release-plugin</artifactId>
+                <version>2.5.2</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.apache.maven.scm</groupId>
+                        <artifactId>maven-scm-provider-gitexe</artifactId>
+                        <version>1.9.4</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+
+        </plugins>
+
+        <extensions>
+            <extension>
+                <groupId>org.apache.maven.wagon</groupId>
+                <artifactId>wagon-ssh-external</artifactId>
+                <version>2.6</version>
+            </extension>
+        </extensions>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>svjatoslav.eu</id>
+            <name>svjatoslav.eu</name>
+            <url>scpexe://svjatoslav.eu:10006/srv/maven</url>
+        </snapshotRepository>
+        <repository>
+            <id>svjatoslav.eu</id>
+            <name>svjatoslav.eu</name>
+            <url>scpexe://svjatoslav.eu:10006/srv/maven</url>
+        </repository>
+    </distributionManagement>
+
+    <scm>
+        <connection>scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/cli-helper.git</connection>
+        <developerConnection>scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/cli-helper.git</developerConnection>
+        <tag>HEAD</tag>
+    </scm>
+
+</project>
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 (executable)
index 0000000..ae0ad34
--- /dev/null
@@ -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;
+
+/**
+ * <h2>CLIHelper</h2>
+ * <p>
+ * 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, <em>nullable</em> results,
+ * value‑range enforcement, and context‑sensitive validation rules (e.g. file
+ * attributes).
+ * </p>
+ *
+ * <h3>Design highlights</h3>
+ * <ul>
+ *   <li>Uses a <strong>single</strong> {@link Scanner} instance bound to {@code System.in}
+ *       to avoid resource‑leak warnings and to ensure scanners do not compete for
+ *       the same underlying input stream.</li>
+ *   <li>All methods are <em>blocking</em>; they return only when a syntactically and
+ *       semantically valid value has been supplied, or when the API contract
+ *       explicitly allows an empty/{@code null} result.</li>
+ *   <li>Every prompt automatically appends the current default value in square
+ *       brackets (e.g. {@code [42]}) so the user can see what will be chosen on
+ *       an empty line.</li>
+ * </ul>
+ *
+ * <h3>Usage example</h3>
+ * <pre>{@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);
+ * }</pre>
+ */
+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.
+     * <p>
+     * Accepted affirmative tokens (case‑insensitive): <kbd>y</kbd>, <kbd>yes</kbd>,
+     * <kbd>true</kbd>.<br>
+     * Accepted negative tokens: <kbd>n</kbd>, <kbd>no</kbd>, <kbd>false</kbd>.
+     * </p>
+     *
+     * <p>If the user submits an empty line:</p>
+     * <ul>
+     *   <li>If {@code defaultValue != null} – return that value.</li>
+     *   <li>Else if {@code allowEmpty == true} – return {@code null}.</li>
+     *   <li>Otherwise the prompt is displayed again.</li>
+     * </ul>
+     *
+     * @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 <strong>file path</strong> and validates various
+     * attributes (existence, readability, writability, executability).
+     * <p>
+     * A {@link File} object is <em>returned as‑is</em>; no attempt is made to
+     * canonicalise or resolve symlinks.
+     * </p>
+     *
+     * @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 <em>not</em> 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 <strong>directory path</strong> 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 <em>not</em> 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 (executable)
index 0000000..eb33fa7
--- /dev/null
@@ -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:
+ * <ul>
+ *   <li>whether it's mandatory</li>
+ *   <li>the number of parameters accepted</li>
+ *   <li>a textual description</li>
+ *   <li>aliases (the actual CLI flags, e.g., "--help" or "-h")</li>
+ * </ul>
+ *
+ * @param <T> the type of object returned by {@link #getValue()}
+ * @param <I> the actual subclass type, used to enable fluent method chaining
+ */
+public abstract class Option<T, I extends Option<?, I>> {
+
+    /**
+     * 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<String> 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<String> 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() ? "<no aliases>" : 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 (executable)
index 0000000..1a30fe8
--- /dev/null
@@ -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 (executable)
index 0000000..41c6b40
--- /dev/null
@@ -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.
+ * <p>
+ * Usage:
+ * <pre>{@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();
+ * }</pre>
+ */
+public class Parser {
+
+    private final List<Option<?, ? extends Option<?, ?>>> options = new ArrayList<>();
+
+    /**
+     * Registers an option with this parser.
+     *
+     * @param option the option to be added
+     * @param <E>    the concrete type of the option
+     * @return the same option object, for chaining
+     */
+    public <E extends Option<?, E>> 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 (executable)
index 0000000..63c1714
--- /dev/null
@@ -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<File, DirectoryOption> {
+
+    /** 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 (executable)
index 0000000..fb0d923
--- /dev/null
@@ -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<List<File>, 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<File> 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 (executable)
index 0000000..a3cccc1
--- /dev/null
@@ -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 (executable)
index 0000000..00c295d
--- /dev/null
@@ -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<File, FileOption> {
+
+    /** 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 (executable)
index 0000000..2d0e8ab
--- /dev/null
@@ -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<List<File>, 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<File> 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 (file)
index 0000000..8aaca36
--- /dev/null
@@ -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<Float, FloatOption> {
+
+    /**
+     * 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 (executable)
index 0000000..3fd6ead
--- /dev/null
@@ -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<Integer, IntegerOption> {
+
+    /**
+     * 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 (executable)
index 0000000..cdf6583
--- /dev/null
@@ -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<Boolean, NullOption> {
+
+    /**
+     * 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 (executable)
index 0000000..fa7f102
--- /dev/null
@@ -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<String, StringOption> {
+
+    /**
+     * 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 (file)
index 0000000..59c0230
--- /dev/null
@@ -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<List<String>, 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<String> 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 (executable)
index 0000000..949e255
--- /dev/null
@@ -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 (executable)
index 0000000..057b511
--- /dev/null
@@ -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 (executable)
index 0000000..de9bae5
--- /dev/null
@@ -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 (executable)
index 0000000..a630418
--- /dev/null
@@ -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