Browse Source

Standardise command line option parsing (#6036)

* Separates long & short options for parsing

* Adds parsing for short options + doc rewrites

* Fixes undefined constant in check.translation

* Standardises CL option parsing

* Refactors option parsing

* Renames getLongOptions -> getOptions

* Removes unused code

* Converges on string typing for options

* Updates docs & help files

* Updates array syntax array( ) -> [ ]
Kasimir Cash 2 years ago
parent
commit
6d14813840

+ 54 - 46
cli/README.md

@@ -35,22 +35,22 @@ cd /usr/share/FreshRSS
 # Ensure the needed directories in ./data/
 
 ./cli/do-install.php --default-user admin [ --auth-type form --environment production --base-url https://rss.example.net --language en --title FreshRSS --allow-anonymous --allow-anonymous-refresh --api-enabled --db-type sqlite --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss_ ]
-# --default-user must be alphanumeric and not longer than 38 characters. The default user of this FreshRSS instance, used as the public user for anonymous reading
-# --auth-type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous)
-# --environment can be: 'production' (default), 'development' (for additional log messages)
-# --base-url should be a public (routable) URL if possible, and is used for push (WebSub), for some API functions (e.g. favicons), and external URLs in FreshRSS
-# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
-# --title web user interface title for this FreshRSS instance
-# --allow-anonymous sets whether non logged-in visitors are permitted to see the default user's feeds
-# --allow-anonymous-refresh sets whether to permit anonymous users to start the refresh process
-# --api-enabled sets whether the API may be used for mobile apps. API passwords must be set for individual users
-# --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
-# --db-host URL of the database server. Default is 'localhost'
-# --db-user sets database user
-# --db-password sets database password
-# --db-base sets database name
-# --db-prefix is an optional prefix in front of the names of the tables. We suggest using 'freshrss_' (default)
-# This command does not create the default user. Do that with ./cli/create-user.php
+# --default-user must be alphanumeric and not longer than 38 characters. The default user of this FreshRSS instance, used as the public user for anonymous reading.
+# --auth-type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous).
+# --environment can be: 'production' (default), 'development' (for additional log messages).
+# --base-url should be a public (routable) URL if possible, and is used for push (WebSub), for some API functions (e.g. favicons), and external URLs in FreshRSS.
+# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/).
+# --title web user interface title for this FreshRSS instance.
+# --allow-anonymous sets whether non logged-in visitors are permitted to see the default user's feeds.
+# --allow-anonymous-refresh sets whether to permit anonymous users to start the refresh process.
+# --api-enabled sets whether the API may be used for mobile apps. API passwords must be set for individual users.
+# --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL).
+# --db-host URL of the database server. Default is 'localhost'.
+# --db-user sets database user.
+# --db-password sets database password.
+# --db-base sets database name.
+# --db-prefix is an optional prefix in front of the names of the tables. We suggest using 'freshrss_' (default).
+# This command does not create the default user. Do that with ./cli/create-user.php.
 
 ./cli/reconfigure.php
 # Same parameters as for do-install.php. Used to update an existing installation.
@@ -64,51 +64,52 @@ cd /usr/share/FreshRSS
 cd /usr/share/FreshRSS
 
 ./cli/create-user.php --user username [ --password 'password' --api-password 'api_password' --language en --email user@example.net --token 'longRandomString' --no-default-feeds --purge-after-months 3 --feed-min-articles-default 50 --feed-ttl-default 3600 --since-hours-posts-per-rss 168 --max-posts-per-rss 400 ]
-# --user must be alphanumeric, not longer than 38 characters. The name of the user to be created/updated
-# --password sets the user's password
-# --api-password sets the user's api password
-# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
-# --email sets an email for the user which will be used email validation if it forced email validation is enabled
-# --no-default-feeds do not add this FreshRSS instance's default feeds to the user during creation
-# --purge-after-months max age an article can reach before being archived. Default is '3'
-# --feed-min-articles-default number of articles in a feed at which archiving will pause. Default is '50'
-# --feed-ttl-default minimum number of seconds to elapse between feed refreshes. Default is '3600'
-# --max-posts-per-rss number of articles in a feed at which an old article will be archived before a new article is added. Default is '200' 
+# --user must be alphanumeric, not longer than 38 characters. The name of the user to be created/updated.
+# --password sets the user's password.
+# --api-password sets the user's api password.
+# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/).
+# --email sets an email for the user which will be used email validation if it forced email validation is enabled.
+# --no-default-feeds do not add this FreshRSS instance's default feeds to the user during creation.
+# --purge-after-months max age an article can reach before being archived. Default is '3'.
+# --feed-min-articles-default number of articles in a feed at which archiving will pause. Default is '50'.
+# --feed-ttl-default minimum number of seconds to elapse between feed refreshes. Default is '3600'.
+# --max-posts-per-rss number of articles in a feed at which an old article will be archived before a new article is added. Default is '200'.
 
 ./cli/update-user.php --user username [ ... ]
-# Same options as create-user.php, except --no-default-feeds which is only available for create-user.php
+# Same options as create-user.php, except --no-default-feeds which is only available for create-user.php.
 ```
 
 > ℹ️ More options for [the configuration of users](../config-user.default.php#L3-L5) may be set in `./data/config-user.custom.php` prior to creating new users, or in `./data/users/*/config.php` for existing users.
 
 ```sh
 ./cli/actualize-user.php --user username
-# Fetch feeds for the specified user
+# Fetch feeds for the specified user.
 
 ./cli/delete-user.php --user username
+# Deletes the specified user.
 
 ./cli/list-users.php
-# Return a list of users, with the default/admin user first
+# Return a list of users, with the default/admin user first.
 
-./cli/user-info.php [ -h --header --json --user username1 --user username2 ... ]
-# -h is to use a human-readable format
-# --header outputs some columns headers
-# --json JSON format (disables --header and -h but uses ISO Zulu format for dates)
-# --user indicates a username, and can be repeated
+./cli/user-info.php [ --human-readable --header --json --user username1 --user username2 ... ]
+# -h, --human-readable display output in a human readable format
+# --header outputs some columns headers.
+# --json JSON format (disables --header and --human-readable but uses ISO Zulu format for dates).
+# --user indicates a username, and can be repeated.
 # Returns: 1) a * if the user is admin, 2) the name of the user,
 #  3) the date/time of last user action, 4) the size occupied,
 #  and the number of: 5) categories, 6) feeds, 7) read articles, 8) unread articles, 9) favourites, 10) tags,
-#  11) language, 12) e-mail
+#  11) language, 12) e-mail.
 
 ./cli/import-for-user.php --user username --filename /path/to/file.ext
-# The extension of the file { .json, .opml, .xml, .zip } is used to detect the type of import
+# The extension of the file { .json, .opml, .xml, .zip } is used to detect the type of import.
 
 ./cli/export-sqlite-for-user.php --user username --filename /path/to/db.sqlite
 # Export the user’s database to a new SQLite file.
 
 ./cli/import-sqlite-for-user.php --user username [ --force-overwrite ] --filename /path/to/db.sqlite
 # Import the user’s database from an SQLite file.
-# --force-overwrite will clear the target user database before import (import only works on an empty user database)
+# --force-overwrite will clear the target user database before import (import only works on an empty user database).
 
 ./cli/export-opml-for-user.php --user username > /path/to/file.opml.xml
 
@@ -129,15 +130,22 @@ cd /usr/share/FreshRSS
 ```sh
 cd /usr/share/FreshRSS
 
-./cli/manipulate.translation.php  --a [-h --a --k --v --l --o]
-# manipulate the i18n language files
-# -h is to use a human-readable format
-# --a selects the action to perform. (can be: add, delete, exist, format, and ignore.
-# --k selects the key to work on.
-# --v selects the value to set.
-# --l selects the language to work on.
-# --r revert the action (only for ignore action)
-# --o selects the origin language (only for add language action)
+./cli/manipulate.translation.php  --action [ --help --key --value --language --revert --origin-language ]
+# manipulate translation files.
+# -a, --action  selects the action to perform. (can be either: add, delete, exist, format, or ignore)
+# -h, --help displays the commands help file.
+# -k, --key selects the key to work on.
+# -v, --value selects the value to set.
+# -l, --language selects the language to work on.
+# -r, --revert revert the action (only used with ignore action).
+# -o, --origin-language selects the origin language (only used with add language action).
+
+./cli/check-translation.php [ ---display-result --help --language fr --display-report ]
+# Check if translation files have missing keys or missing translations.
+# -d, --display-result display results of check.
+# -h, --help display help text and exit.
+# -l, --language set the language check.
+# -r, --display-report display completion report.
 ```
 
 ## Note about cron

+ 46 - 67
cli/_cli.php

@@ -6,8 +6,7 @@ if (php_sapi_name() !== 'cli') {
 }
 
 const EXIT_CODE_ALREADY_EXISTS = 3;
-const REGEX_INPUT_OPTIONS = '/^--/';
-const REGEX_PARAM_OPTIONS = '/:*$/';
+const REGEX_INPUT_OPTIONS = '/^-{2}|^-{1}/';
 
 require(__DIR__ . '/../constants.php');
 require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
@@ -77,37 +76,43 @@ function performRequirementCheck(string $databaseType): void {
 
 /**
  * Parses parameters used with FreshRSS' CLI commands.
- * @param array{'valid':array<string,string>,'deprecated':array<string,string>} $parameters An array of 'valid': An
- * array of parameters as keys and their respective getopt() notations as values. 'deprecated' An array with
- * replacement parameters as keys and their respective deprecated parameters as values.
- * @return array{'valid':array<string,string|bool>,'invalid':array<string>} An array of 'valid': an array of all
- * known parameters used and their respective options and 'invalid': an array of all unknown parameters used.
+ * @param array{'long':array<string,string>,'short':array<string,string>,'deprecated':array<string,string>} $parameters
+ * Matrix of 'long': map of long option names as keys and their respective getopt() notations as values,
+ * 'short': map of short option names as values and their equivalent long options as keys, 'deprecated': map of
+ * replacement option names as keys and their respective deprecated option names as values.
+ * @return array{'valid':array<string,string>,'invalid':array<string>} Matrix of 'valid': map of of all known
+ * option names used and their respective values and 'invalid': list of all unknown options used.
  */
 function parseCliParams(array $parameters): array {
 	global $argv;
-	$cliParams = [];
+	$longOptions = [];
+	$shortOptions = '';
 
-	foreach ($parameters['valid'] as $param => $getopt_val) {
-		$cliParams[] = $param . $getopt_val;
+	foreach ($parameters['long'] as $name => $getopt_note) {
+		$longOptions[] = $name . $getopt_note;
 	}
-	foreach ($parameters['deprecated'] as $param => $deprecatedParam) {
-		$cliParams[] = $deprecatedParam . $parameters['valid'][$param];
+	foreach ($parameters['deprecated'] as $name => $deprecatedName) {
+		$longOptions[] = $deprecatedName . $parameters['long'][$name];
+	}
+	foreach ($parameters['short'] as $name => $shortName) {
+		$shortOptions .= $shortName . $parameters['long'][$name];
 	}
 
-	$opts = getopt('', $cliParams);
+	$options = getopt($shortOptions, $longOptions);
 
-	/** @var array<string,string|bool> $valid */
-	$valid = is_array($opts) ? $opts : [];
+	$valid = is_array($options) ? $options : [];
 
-	array_walk($valid, static fn(&$option) => $option = $option === false ? true : $option);
+	array_walk($valid, static fn(&$option) => $option = $option === false ? '' : $option);
 
-	if (checkforDeprecatedParameterUse(array_keys($valid), $parameters['deprecated'])) {
-		$valid = updateDeprecatedParameters($valid, $parameters['deprecated']);
-	}
+	/** @var array<string,string> $valid */
+	checkForDeprecatedOptions(array_keys($valid), $parameters['deprecated']);
+
+	$valid = replaceOptions($valid, $parameters['short']);
+	$valid = replaceOptions($valid, $parameters['deprecated']);
 
 	$invalid = findInvalidOptions(
 		$argv,
-		array_merge(array_keys($parameters['valid']), array_values($parameters['deprecated']))
+		array_merge(array_keys($parameters['long']), array_values($parameters['short']), array_values($parameters['deprecated']))
 	);
 
 	return [
@@ -120,7 +125,7 @@ function parseCliParams(array $parameters): array {
  * @param array<string> $options
  * @return array<string>
  */
-function getLongOptions(array $options, string $regex): array {
+function getOptions(array $options, string $regex): array {
 	$longOptions = array_filter($options, static function (string $a) use ($regex) {
 		return preg_match($regex, $a) === 1;
 	});
@@ -130,30 +135,13 @@ function getLongOptions(array $options, string $regex): array {
 }
 
 /**
- * @param array<string> $input
- * @param array<string> $params
- */
-function validateOptions(array $input, array $params): bool {
-	$sanitizeInput = getLongOptions($input, REGEX_INPUT_OPTIONS);
-	$sanitizeParams = getLongOptions($params, REGEX_PARAM_OPTIONS);
-	$unknownOptions = array_diff($sanitizeInput, $sanitizeParams);
-
-	if (0 === count($unknownOptions)) {
-		return true;
-	}
-
-	fwrite(STDERR, sprintf("FreshRSS error: unknown options: %s\n", implode (', ', $unknownOptions)));
-	return false;
-}
-
-/**
- * Checks for use of unknown parameters with FreshRSS' CLI commands.
- * @param array<string> $input An array of parameters to check for validity.
- * @param array<string> $params An array of valid parameters to check against.
- * @return array<string> Returns an array of all unknown parameters found.
+ * Checks for presence of unknown options.
+ * @param array<string> $input List of command line arguments to check for validity.
+ * @param array<string> $params List of valid options to check against.
+ * @return array<string> Returns a list all unknown options found.
  */
 function findInvalidOptions(array $input, array $params): array {
-	$sanitizeInput = getLongOptions($input, REGEX_INPUT_OPTIONS);
+	$sanitizeInput = getOptions($input, REGEX_INPUT_OPTIONS);
 	$unknownOptions = array_diff($sanitizeInput, $params);
 
 	if (0 === count($unknownOptions)) {
@@ -165,15 +153,14 @@ function findInvalidOptions(array $input, array $params): array {
 }
 
 /**
- * Checks for use of deprecated parameters with FreshRSS' CLI commands.
- * @param array<string> $options User inputs to check for deprecated parameter use.
- * @param array<string,string> $params An array with replacement parameters as keys and their respective deprecated
- * parameters as values.
- * @return bool Returns TRUE and generates a deprecation warning if deprecated parameters
- * have been used, FALSE otherwise.
+ * Checks for presence of deprecated options.
+ * @param array<string> $optionNames Command line option names to check for deprecation.
+ * @param array<string,string> $params Map of replacement options as keys and their respective deprecated
+ * options as values.
+ * @return bool Returns TRUE and generates a deprecation warning if deprecated options are present, FALSE otherwise.
  */
-function checkforDeprecatedParameterUse(array $options, array $params): bool {
-	$deprecatedOptions = array_intersect($options, $params);
+function checkForDeprecatedOptions(array $optionNames, array $params): bool {
+	$deprecatedOptions = array_intersect($optionNames, $params);
 	$replacements = array_map(static fn($option) => array_search($option, $params, true), $deprecatedOptions);
 
 	if (0 === count($deprecatedOptions)) {
@@ -187,25 +174,17 @@ function checkforDeprecatedParameterUse(array $options, array $params): bool {
 }
 
 /**
- * Switches all used deprecated parameters to their replacements if they have one.
- *
- * @template T
- *
- * @param array<string,T> $options User inputs.
- * @param array<string,string> $params An array with replacement parameters as keys and their respective deprecated
- * parameters as values.
- * @return array<string,T>  Returns $options with deprications replaced.
+ * Switches items in a list to their provided replacements.
+ * @param array<string,string> $options Map with items to check for replacement as keys.
+ * @param array<string,string> $replacements Map of replacement items as keys and the item they replace as their values.
+ * @return array<string,string>  Returns $options with replacements.
  */
-function updateDeprecatedParameters(array $options, array $params): array {
+function replaceOptions(array $options, array $replacements): array {
 	$updatedOptions = [];
 
-	foreach ($options as $param => $option) {
-		$replacement = array_search($param, $params, true);
-		if (is_string($replacement)) {
-			$updatedOptions[$replacement] = $option;
-		} else {
-			$updatedOptions[$param] = $option;
-		}
+	foreach ($options as $name => $value) {
+		$replacement = array_search($name, $replacements, true);
+		$updatedOptions[$replacement ? $replacement : $name] = $value;
 	}
 
 	return $updatedOptions;

+ 8 - 7
cli/_update-or-create-user.php

@@ -4,8 +4,8 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$parameters = array(
-	'valid' => array(
+$parameters = [
+	'long' => [
 		'user' => ':',
 		'password' => ':',
 		'api-password' => ':',
@@ -17,21 +17,22 @@ $parameters = array(
 		'feed-ttl-default' => ':',
 		'since-hours-posts-per-rss' => ':',
 		'max-posts-per-rss' => ':',
-	),
-	'deprecated' => array(
+	],
+	'short' => [],
+	'deprecated' => [
 		'api-password' => 'api_password',
 		'purge-after-months' => 'purge_after_months',
 		'feed-min-articles-default' => 'feed_min_articles_default',
 		'feed-ttl-default' => 'feed_ttl_default',
 		'since-hours-posts-per-rss' => 'since_hours_posts_per_rss',
 		'max-posts-per-rss' => 'max_posts_per_rss',
-	),
-);
+	],
+];
 
 if (!isset($isUpdate)) {
 	$isUpdate = false;
 } elseif (!$isUpdate) {
-	$parameters['valid']['no-default-feeds'] = '';	//Only for creating new users
+	$parameters['long']['no-default-feeds'] = '';	//Only for creating new users
 	$parameters['deprecated']['no-default-feeds'] = 'no_default_feeds';
 }
 

+ 10 - 6
cli/actualize-user.php

@@ -5,17 +5,21 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = array(
-	'user:',
-);
+$parameters = [
+	'long' => [
+		'user' => ':'
+	],
+	'short' => [],
+	'deprecated' => [],
+];
 
-$options = getopt('', $params);
+$options = parseCliParams($parameters);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
+if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username");
 }
 
-$username = cliInitUser($options['user']);
+$username = cliInitUser($options['valid']['user']);
 
 Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
 

+ 29 - 12
cli/check.translation.php

@@ -1,28 +1,45 @@
 #!/usr/bin/env php
 <?php
 declare(strict_types=1);
+require_once __DIR__ . '/_cli.php';
 require_once __DIR__ . '/i18n/I18nCompletionValidator.php';
 require_once __DIR__ . '/i18n/I18nData.php';
 require_once __DIR__ . '/i18n/I18nFile.php';
 require_once __DIR__ . '/i18n/I18nUsageValidator.php';
+require_once __DIR__ . '/../constants.php';
 
 $i18nFile = new I18nFile();
 $i18nData = new I18nData($i18nFile->load());
 
-/** @var array<string,string>|false $options */
-$options = getopt('dhl:r');
-
-if (!is_array($options) || array_key_exists('h', $options)) {
+$parameters = [
+	'long' => [
+		'display-result' => '',
+		'help' => '',
+		'language' => ':',
+		'display-report' => '',
+	],
+	'short' => [
+		'display-result' => 'd',
+		'help' => 'h',
+		'language' => 'l',
+		'display-report' => 'r',
+	],
+	'deprecated' => [],
+];
+
+$options = parseCliParams($parameters);
+
+if (!empty($options['invalid']) || array_key_exists('help', $options['valid'])) {
 	checkHelp();
 }
 
-if (array_key_exists('l', $options)) {
-	$languages = array($options['l']);
+if (array_key_exists('language', $options['valid'])) {
+	$languages = [$options['valid']['language']];
 } else {
 	$languages = $i18nData->getAvailableLanguages();
 }
-$displayResults = array_key_exists('d', $options);
-$displayReport = array_key_exists('r', $options);
+$displayResults = array_key_exists('display-result', $options['valid']);
+$displayReport = array_key_exists('display-report', $options['valid']);
 
 $isValidated = true;
 $result = [];
@@ -99,10 +116,10 @@ SYNOPSIS
 DESCRIPTION
 	Check if translation files have missing keys or missing translations.
 
-	-d	display results.
-	-h	display this help and exit.
-	-l=LANG	filter by LANG.
-	-r	display completion report.
+	-d, --display-result	display results.
+	-h, --help		display this help and exit.
+	-l, --language=LANG	filter by LANG.
+	-r, --display-report	display completion report.
 
 HELP;
 	exit;

+ 10 - 6
cli/db-optimize.php

@@ -5,17 +5,21 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = array(
-	'user:',
-);
+$parameters = [
+	'long' => [
+		'user' => ':',
+	],
+	'short' => [],
+	'deprecated' => [],
+];
 
-$options = getopt('', $params);
+$options = parseCliParams($parameters);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
+if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username");
 }
 
-$username = cliInitUser($options['user']);
+$username = cliInitUser($options['valid']['user']);
 
 echo 'FreshRSS optimizing database for user “', $username, "”…\n";
 

+ 10 - 6
cli/delete-user.php

@@ -5,16 +5,20 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = array(
-	'user:',
-);
+$parameters = [
+	'long' => [
+		'user' => ':',
+	],
+	'short' => [],
+	'deprecated' => [],
+];
 
-$options = getopt('', $params);
+$options = parseCliParams($parameters);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
+if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username");
 }
-$username = $options['user'];
+$username = $options['valid']['user'];
 if (!FreshRSS_user_Controller::checkUsername($username)) {
 	fail('FreshRSS error: invalid username “' . $username . '”');
 }

+ 13 - 11
cli/do-install.php

@@ -7,8 +7,8 @@ if (file_exists(DATA_PATH . '/applied_migrations.txt')) {
 	fail('FreshRSS seems to be already installed!' . "\n" . 'Please use `./cli/reconfigure.php` instead.', EXIT_CODE_ALREADY_EXISTS);
 }
 
-$parameters = array(
-	'valid' => array(
+$parameters = [
+	'long' => [
 		'environment' => ':',
 		'base-url' => ':',
 		'language' => ':',
@@ -26,8 +26,9 @@ $parameters = array(
 		'db-password' => ':',
 		'db-base' => ':',
 		'db-prefix' => '::',
-	),
-	'deprecated' => array(
+	],
+	'short' => [],
+	'deprecated' => [
 		'base-url' => 'base_url',
 		'default-user' => 'default_user',
 		'allow-anonymous' => 'allow_anonymous',
@@ -36,10 +37,10 @@ $parameters = array(
 		'api-enabled' => 'api_enabled',
 		'allow-robots' => 'allow_robots',
 		'disable-update' => 'disable_update',
-	),
-);
+	],
+];
 
-$configParams = array(
+$configParams = [
 	'environment' => 'environment',
 	'base-url' => 'base_url',
 	'language' => 'language',
@@ -51,16 +52,16 @@ $configParams = array(
 	'api-enabled' => 'api_enabled',
 	'allow-robots' => 'allow_robots',
 	'disable-update' => 'disable_update',
-);
+];
 
-$dBconfigParams = array(
+$dBconfigParams = [
 	'db-type' => 'type',
 	'db-host' => 'host',
 	'db-user' => 'user',
 	'db-password' => 'password',
 	'db-base' => 'base',
 	'db-prefix' => 'prefix',
-);
+];
 
 $options = parseCliParams($parameters);
 
@@ -89,7 +90,8 @@ if (file_exists($customConfigPath)) {
 
 foreach ($configParams as $param => $configParam) {
 	if (isset($options['valid'][$param])) {
-		$config[$configParam] = $options['valid'][$param];
+		$isFlag = $parameters['long'][$param] === '';
+		$config[$configParam] = $isFlag ? true : $options['valid'][$param];
 	}
 }
 

+ 10 - 6
cli/export-opml-for-user.php

@@ -5,17 +5,21 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = array(
-	'user:',
-);
+$parameters = [
+	'long' => [
+		'user' => ':',
+	],
+	'short' => [],
+	'deprecated' => [],
+];
 
-$options = getopt('', $params);
+$options = parseCliParams($parameters);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
+if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
 }
 
-$username = cliInitUser($options['user']);
+$username = cliInitUser($options['valid']['user']);
 
 fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
 

+ 14 - 7
cli/export-sqlite-for-user.php

@@ -5,19 +5,26 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = [
-	'user:',
-	'filename:',
+$parameters = [
+	'long' => [
+		'user' => ':',
+		'filename' => ':',
+	],
+	'short' => [],
+	'deprecated' => [],
 ];
 
-$options = getopt('', $params);
+$options = parseCliParams($parameters);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename']) || !is_string($options['user']) || !is_string($options['filename'])) {
+if (!empty($options['invalid'])
+	|| empty($options['valid']['user']) || empty($options['valid']['filename'])
+	|| !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
+) {
 	fail('Usage: ' . basename(__FILE__) . ' --user username --filename /path/to/db.sqlite');
 }
 
-$username = cliInitUser($options['user']);
-$filename = $options['filename'];
+$username = cliInitUser($options['valid']['user']);
+$filename = $options['valid']['filename'];
 
 if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
 	fail('Only *.sqlite files are supported!');

+ 14 - 10
cli/export-zip-for-user.php

@@ -5,14 +5,18 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = array(
-	'user:',
-	'max-feed-entries:',
-);
-
-$options = getopt('', $params);
-
-if (!validateOptions($argv, $params) || empty($options['user']) || !is_string($options['user'])) {
+$parameters = [
+	'long' => [
+		'user' => ':',
+		'max-feed-entries' => ':',
+	],
+	'short' => [],
+	'deprecated' => [],
+];
+
+$options = parseCliParams($parameters);
+
+if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
 	fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
 }
 
@@ -20,12 +24,12 @@ if (!extension_loaded('zip')) {
 	fail('FreshRSS error: Lacking php-zip extension!');
 }
 
-$username = cliInitUser($options['user']);
+$username = cliInitUser($options['valid']['user']);
 
 fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
 
 $export_service = new FreshRSS_Export_Service($username);
-$number_entries = empty($options['max-feed-entries']) ? 100 : intval($options['max-feed-entries']);
+$number_entries = empty($options['valid']['max-feed-entries']) ? 100 : intval($options['valid']['max-feed-entries']);
 $exported_files = [];
 
 // First, we generate the OPML file

+ 17 - 10
cli/import-for-user.php

@@ -5,20 +5,27 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = array(
-	'user:',
-	'filename:',
-);
-
-$options = getopt('', $params);
-
-if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename']) || !is_string($options['user']) || !is_string($options['filename'])) {
+$parameters = [
+	'long' => [
+		'user' => ':',
+		'filename' => ':',
+	],
+	'short' => [],
+	'deprecated' => [],
+];
+
+$options = parseCliParams($parameters);
+
+if (!empty($options['invalid'])
+	|| empty($options['valid']['user']) || empty($options['valid']['filename'])
+	|| !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
+) {
 	fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
 }
 
-$username = cliInitUser($options['user']);
+$username = cliInitUser($options['valid']['user']);
 
-$filename = $options['filename'];
+$filename = $options['valid']['filename'];
 if (!is_readable($filename)) {
 	fail('FreshRSS error: file is not readable “' . $filename . '”');
 }

+ 16 - 9
cli/import-sqlite-for-user.php

@@ -5,20 +5,27 @@ require(__DIR__ . '/_cli.php');
 
 performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
 
-$params = [
-	'user:',
-	'filename:',
-	'force-overwrite',
+$parameters = [
+	'long' => [
+		'user' => ':',
+		'filename' => ':',
+		'force-overwrite' => '',
+	],
+	'short' => [],
+	'deprecated' => [],
 ];
 
-$options = getopt('', $params);
+$options = parseCliParams($parameters);
 
-if (!validateOptions($argv, $params) || empty($options['user']) || empty($options['filename']) || !is_string($options['user']) || !is_string($options['filename'])) {
+if (!empty($options['invalid'])
+	|| empty($options['valid']['user']) || empty($options['valid']['filename'])
+	|| !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
+) {
 	fail('Usage: ' . basename(__FILE__) . ' --user username --force-overwrite --filename /path/to/db.sqlite');
 }
 
-$username = cliInitUser($options['user']);
-$filename = $options['filename'];
+$username = cliInitUser($options['valid']['user']);
+$filename = $options['valid']['filename'];
 
 if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
 	fail('Only *.sqlite files are supported!');
@@ -27,7 +34,7 @@ if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
 echo 'FreshRSS importing database from SQLite for user “', $username, "”…\n";
 
 $databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
-$clearFirst = array_key_exists('force-overwrite', $options);
+$clearFirst = array_key_exists('force-overwrite', $options['valid']);
 $ok = $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_IMPORT, $clearFirst);
 if (!$ok) {
 	echo 'If you would like to clear the user database first, use the option --force-overwrite', "\n";

+ 65 - 41
cli/manipulate.translation.php

@@ -1,53 +1,75 @@
 #!/usr/bin/env php
 <?php
 declare(strict_types=1);
+require_once __DIR__ . '/_cli.php';
 require_once __DIR__ . '/i18n/I18nData.php';
 require_once __DIR__ . '/i18n/I18nFile.php';
 require_once __DIR__ . '/../constants.php';
 
-/** @var array<string,string>|false $options */
-$options = getopt('a:hk:l:o:rv:');
-
-if (!is_array($options) || array_key_exists('h', $options)) {
+$parameters = [
+	'long' => [
+		'action' => ':',
+		'help' => '',
+		'key' => ':',
+		'language' => ':',
+		'origin-language' => ':',
+		'revert' => '',
+		'value' => ':',
+	],
+	'short' => [
+		'action' => 'a',
+		'help' => 'h',
+		'key' => 'k',
+		'language' => 'l',
+		'origin-language' => 'o',
+		'revert' => 'r',
+		'value' => 'v',
+	],
+	'deprecated' => [],
+];
+
+$options = parseCliParams($parameters);
+
+if (!empty($options['invalid']) || array_key_exists('help', $options['valid'])) {
 	manipulateHelp();
 	exit();
 }
 
-if (!array_key_exists('a', $options)) {
+if (!array_key_exists('action', $options['valid'])) {
 	error('You need to specify the action to perform.');
 }
 
 $data = new I18nFile();
 $i18nData = new I18nData($data->load());
 
-switch ($options['a']) {
+switch ($options['valid']['action']) {
 	case 'add' :
-		if (array_key_exists('k', $options) && array_key_exists('v', $options) && array_key_exists('l', $options)) {
-			$i18nData->addValue($options['k'], $options['v'], $options['l']);
-		} elseif (array_key_exists('k', $options) && array_key_exists('v', $options)) {
-			$i18nData->addKey($options['k'], $options['v']);
-		} elseif (array_key_exists('l', $options)) {
+		if (array_key_exists('key', $options['valid']) && array_key_exists('value', $options['valid']) && array_key_exists('language', $options['valid'])) {
+			$i18nData->addValue($options['valid']['key'], $options['valid']['value'], $options['valid']['language']);
+		} elseif (array_key_exists('key', $options['valid']) && array_key_exists('value', $options['valid'])) {
+			$i18nData->addKey($options['valid']['key'], $options['valid']['value']);
+		} elseif (array_key_exists('language', $options['valid'])) {
 			$reference = null;
-			if (array_key_exists('o', $options)) {
-				$reference = $options['o'];
+			if (array_key_exists('origin-language', $options['valid'])) {
+				$reference = $options['valid']['origin-language'];
 			}
-			$i18nData->addLanguage($options['l'], $reference);
+			$i18nData->addLanguage($options['valid']['language'], $reference);
 		} else {
 			error('You need to specify a valid set of options.');
 			exit;
 		}
 		break;
 	case 'delete' :
-		if (array_key_exists('k', $options)) {
-			$i18nData->removeKey($options['k']);
+		if (array_key_exists('key', $options['valid'])) {
+			$i18nData->removeKey($options['valid']['key']);
 		} else {
 			error('You need to specify the key to delete.');
 			exit;
 		}
 		break;
 	case 'exist':
-		if (array_key_exists('k', $options)) {
-			$key = $options['k'];
+		if (array_key_exists('key', $options['valid'])) {
+			$key = $options['valid']['key'];
 			if ($i18nData->isKnown($key)) {
 				echo "The '{$key}' key is known.\n\n";
 			} else {
@@ -61,16 +83,16 @@ switch ($options['a']) {
 	case 'format' :
 		break;
 	case 'ignore' :
-		if (array_key_exists('l', $options) && array_key_exists('k', $options)) {
-			$i18nData->ignore($options['k'], $options['l'], array_key_exists('r', $options));
+		if (array_key_exists('language', $options['valid']) && array_key_exists('key', $options['valid'])) {
+			$i18nData->ignore($options['valid']['key'], $options['valid']['language'], array_key_exists('revert', $options['valid']));
 		} else {
 			error('You need to specify a valid set of options.');
 			exit;
 		}
 		break;
 	case 'ignore_unmodified' :
-		if (array_key_exists('l', $options)) {
-			$i18nData->ignore_unmodified($options['l'], array_key_exists('r', $options));
+		if (array_key_exists('language', $options['valid'])) {
+			$i18nData->ignore_unmodified($options['valid']['language'], array_key_exists('revert', $options['valid']));
 		} else {
 			error('You need to specify a valid set of options.');
 			exit;
@@ -110,46 +132,48 @@ SYNOPSIS
 DESCRIPTION
 	Manipulate translation files.
 
-	-a=ACTION
-		select the action to perform. Available actions are add, delete,
-		exist, format, ignore, and ignore_unmodified. This option is mandatory.
-	-k=KEY	select the key to work on.
-	-v=VAL	select the value to set.
-	-l=LANG	select the language to work on.
-	-h	display this help and exit.
-	-r revert the action (only for ignore action)
-	-o=LANG select the origin language (only for add language action)
+	-a, --action=ACTION
+				select the action to perform. Available actions are add, delete,
+				exist, format, ignore, and ignore_unmodified. This option is mandatory.
+	-k, --key=KEY		select the key to work on.
+	-v, --value=VAL		select the value to set.
+	-l, --language=LANG	select the language to work on.
+	-h, --help		display this help and exit.
+	-r, --revert		revert the action (only for ignore action)
+	-o, origin-language=LANG
+				select the origin language (only for add language action)
 
 EXAMPLES
-Example 1: add a language. It adds a new language by duplicating the referential.
+Example 1:	add a language. It adds a new language by duplicating the referential.
 	php $file -a add -l my_lang
 	php $file -a add -l my_lang -o ref_lang
 
-Example 2: add a new key. It adds the key for all supported languages.
+Example 2:	add a new key. It adds the key for all supported languages.
 	php $file -a add -k my_key -v my_value
 
-Example 3: add a new value. It adds a new value for the selected key in the selected language.
+Example 3:	add a new value. It adds a new value for the selected key in the selected language.
 	php $file -a add -k my_key -v my_value -l my_lang
 
-Example 4: delete a key. It deletes the selected key from all supported languages.
+Example 4:	delete a key. It deletes the selected key from all supported languages.
 	php $file -a delete -k my_key
 
-Example 5: format i18n files.
+Example 5:	format i18n files.
 	php $file -a format
 
-Example 6: ignore a key. It adds the key in the ignore file to mark it as translated.
+Example 6:	ignore a key. Adds IGNORE comment to the key in the selected language, marking it as translated.
 	php $file -a ignore -k my_key -l my_lang
 
-Example 7: revert ignore a key. It removes the key from the ignore file.
+Example 7:	revert ignore a key. Removes IGNORE comment from the key in the selected language.
 	php $file -a ignore -r -k my_key -l my_lang
 
-Example 8: ignore all unmodified keys. It adds all modified keys in the ignore file to mark it as translated.
+Example 8:	ignore all unmodified keys. Adds IGNORE comments to all unmodified keys in the selected language, marking them as translated.
 	php $file -a ignore_unmodified -l my_lang
 
-Example 9: revert ignore of all unmodified keys. It removes the unmodified keys from the ignore file.  Warning, this will also revert keys added individually.
+Example 9:	revert ignore on all unmodified keys. Removes IGNORE comments from all unmodified keys in the selected language.
+		Warning: will also revert individually added unmodified keys.
 	php $file -a ignore_unmodified -r -l my_lang
 
-Example 10: check if a key exist.
+Example 10:	check if a key exist.
 	php $file -a exist -k my_key\n\n
 
 HELP;

+ 11 - 10
cli/reconfigure.php

@@ -3,8 +3,8 @@
 declare(strict_types=1);
 require(__DIR__ . '/_cli.php');
 
-$parameters = array(
-	'valid' => array(
+$parameters = [
+	'long' => [
 		'environment' => ':',
 		'base-url' => ':',
 		'language' => ':',
@@ -22,8 +22,9 @@ $parameters = array(
 		'db-password' => ':',
 		'db-base' => ':',
 		'db-prefix' => '::',
-	),
-	'deprecated' => array(
+	],
+	'short' => [],
+	'deprecated' => [
 		'base-url' => 'base_url',
 		'default-user' => 'default_user',
 		'allow-anonymous' => 'allow_anonymous',
@@ -32,10 +33,10 @@ $parameters = array(
 		'api-enabled' => 'api_enabled',
 		'allow-robots' => 'allow_robots',
 		'disable-update' => 'disable_update',
-	),
-);
+	],
+];
 
-$configParams = array(
+$configParams = [
 	'environment',
 	'base-url',
 	'language',
@@ -47,16 +48,16 @@ $configParams = array(
 	'api-enabled',
 	'allow-robots',
 	'disable-update',
-);
+];
 
-$dBconfigParams = array(
+$dBconfigParams = [
 	'db-type' => 'type',
 	'db-host' => 'host',
 	'db-user' => 'user',
 	'db-password' => 'password',
 	'db-base' => 'base',
 	'db-prefix' => 'prefix',
-);
+];
 
 $options = parseCliParams($parameters);
 

+ 25 - 17
cli/user-info.php

@@ -5,37 +5,45 @@ require(__DIR__ . '/_cli.php');
 
 const DATA_FORMAT = "%-7s | %-20s | %-5s | %-7s | %-25s | %-15s | %-10s | %-10s | %-10s | %-10s | %-10s | %-10s | %-5s | %-10s\n";
 
-$params = array(
-	'user:',
-	'header',
-	'json',
-);
-$options = getopt('h', $params);
+$parameters = [
+	'long' => [
+		'user' => ':',
+		'header' => '',
+		'json' => '',
+		'human-readable' => '',
+	],
+	'short' => [
+		'human-readable' => 'h',
+	],
+	'deprecated' => [],
+];
 
-if (!validateOptions($argv, $params)) {
-	fail('Usage: ' . basename(__FILE__) . ' (-h --header --json --user username --user username …)');
+$options = parseCliParams($parameters);
+
+if (!empty($options['invalid'])) {
+	fail('Usage: ' . basename(__FILE__) . ' (--human-readable --header --json --user username --user username …)');
 }
 
-if (empty($options['user'])) {
+if (empty($options['valid']['user'])) {
 	$users = listUsers();
-} elseif (is_array($options['user'])) {
+} elseif (is_array($options['valid']['user'])) {
 	/** @var array<string> $users */
-	$users = $options['user'];
+	$users = $options['valid']['user'];
 } else {
 	/** @var array<string> $users */
-	$users = array($options['user']);
+	$users = [$options['valid']['user']];
 }
 
 sort($users);
 
-$formatJson = isset($options['json']);
+$formatJson = isset($options['valid']['json']);
 $jsonOutput = [];
 if ($formatJson) {
-	unset($options['header']);
-	unset($options['h']);
+	unset($options['valid']['header']);
+	unset($options['valid']['human-readable']);
 }
 
-if (array_key_exists('header', $options)) {
+if (array_key_exists('header', $options['valid'])) {
 	printf(
 		DATA_FORMAT,
 		'default',
@@ -84,7 +92,7 @@ foreach ($users as $username) {
 		'lang' => FreshRSS_Context::userConf()->language,
 		'mail_login' => FreshRSS_Context::userConf()->mail_login,
 	);
-	if (isset($options['h'])) {	//Human format
+	if (isset($options['valid']['human-readable'])) {	//Human format
 		$data['last_user_activity'] = date('c', $data['last_user_activity']);
 		$data['database_size'] = format_bytes($data['database_size']);
 	}